مطالب
آموزش MDX Query - قسمت دهم – ادامه کار برروی ساختار های سلسله مراتبی و کار با تابع Cousin و ایجاد Range

در این قسمت در خصوص توابع مرتبط با ساختار سلسله مراتبی صحبت خواهد شد.

Select
{
  [Date].[Calendar].[Calendar Quarter].[Q1 CY 2006],
  cousin(
[Date].[Calendar].[Calendar Quarter].[Q1 CY 2006],
[Date].[Calendar].[Calendar Year].[CY 2007]
)
} on columns,
[Measures].[Reseller Sales Amount] on rows
From [Adventure Works]

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

خروجی به صورت زیر می‌باشد:

خوب حالا به ساختار زیر دقت کنید (ساختار سلسله مراتبی Date  )

همانطور که مشخص می‌باشد تاریخ‌ها از 2005 تا 2008 و سال 2010 می‌باشند و فصول عبارتند از دو فصل پایانی سال 2005 و تمامی فصول سال 2006 و 2007 و سه فصل اول سال 2008 و فصل چهارم سال 2010 . حال دوباره به کوئری نوشته شده دقت کنید. در کوئری بالا فصل همسطح فصل اول  سال 2006 در سال 2007 مورد واکشی قرار گرفته است که همان فصل اول در سال 2007 می‌باشد.

حال به بررس کوئری زیر خواهیم پرداخت:

Select
{
  [Date].[Calendar].[Calendar Quarter].[Q1 CY 2006],
  cousin(
[Date].[Calendar].[Calendar Quarter].[Q1 CY 2006],
[Date].[Calendar].[Calendar Semester].[H2 CY 2006]
)
} on columns,
[Measures].[Reseller Sales Amount] on rows
From [Adventure Works]

در این کوئری ما ابتدا ستون فصل اول سال 2006 را بر می گردانیم . سپس در تابع پسر عمو در نیم فصل دوم سال 2006 به دنبال هم سطح فصل اول 2006 می گردیم .

نمودار درختی زیر توضیح کاملی به ما خواهد داد:

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

Select
{
  [Date].[Calendar].[Calendar Quarter].[Q1 CY 2006],
  [Date].[Calendar].[Calendar Quarter].[Q2 CY 2006],
  [Date].[Calendar].[Calendar Quarter].[Q3 CY 2006]
} on columns,
[Measures].[Reseller Sales Amount] on rows
From [Adventure Works]

و

Select
[Date].[Calendar].[Calendar Quarter].[Q1 CY 2006]:
[Date].[Calendar].[Calendar Quarter].[Q3 CY 2006]
on columns,
[Measures].[Reseller Sales Amount] on rows
From [Adventure Works]

خروجی‌ها به صورت زیر می‌باشد :

و

مشخص می‌باشد که از علامت <:> برای ایجاد یک محدوده و جلوگیری از تولید کد‌های بلند و طولانی استفاده می‌شود.

حال کوئری زیر را اجرا کنید:

Select
[Date].[Calendar].[Calendar Quarter].[Q1 CY 2006]
:
cousin(
[Date].[Calendar].[Calendar Quarter].[Q1 CY 2006],
[Date].[Calendar].[Calendar Semester].[H2 CY 2006]
    ) on columns,
[Measures].[Reseller Sales Amount] on rows
From [Adventure Works]

در این کوئری در ابتدا تابع پسر عمو اجرا می گردد، سپس تابع رنج اجرا می گردد و در نتیجه، فاصله ی بین  Q1 CY 2006 تا Q3 CY 2006 را بدست می‌آورد.

 نمودار درختی زیر توضیح کاملی به ما خواهد داد :

خروجی به صورت زیر می‌باشد

در قسمت‌های بعدی دیگر توابع MDX Query‌ها را بررسی میکنیم.

نظرات مطالب
Protocol Buffers فرمتی برای تبادل دیتا
چند تا مساله را میگم و دیگه این بحث رو ادامه نمیدم به هیچ وجه (چون مساله‌ی مطرح شده هیچ ربطی به این مقاله‌ی خاص نداره و فقط چون این وبسایت بازدید کننده‌ی زیادی داره مجبورم کامل توضیح بدم که ما اشتباها خط غلط به کسی ندیم و گمراه نکنیم کسی رو ناخواسته)
1) موضوع این مقاله فقط و فقط protocol buffers بود و هیچ ربطی به هیچ زبان برنامه نویسی نداشت و وارد شدن به این مساله در اینجا اشتباه است، من حتی در این مقاله توضیح ندادم که با استفاده از grpc و protobuf مستقیما نمیتونیم از طریق browser درخواست بدیم، و وارد کردن این مساله و همینطور لایبری‌های مختلف جاوا اسکریپتی و سی شارپی جایگاهش در این نقاله نبود چون نگاه خیلی بالاتر از این بود تا اینکه به سطح بیاییم و بخواهیم راجع به یک کتابخونه خاص و یک زبان خاص و مقایسه بین آنها رو انجام بدیم!
2) از اینجا میتونید مشاهد کنید که حداقل به صورت رسمی (اینکه توی گوگل سرچ زده بشه و اولین نتیجه به عنوان سند در پاسخ داده بشه مورد قبول نیست، چون قطعا ممکنه به صورت غیر رسمی ابزار هایی باشه که مورد قبول در تولید یک سیستم روی پروداکشن نیست!) زبان‌های swift, rust , ... هنوز به صورت رسمی ساپورت نمیشند! پس اگر نگاهمون به عنوان توسعه دهنده فراتر از یک مثال hello-world باشه باید اینو مد نظر بگیریم.
3) تکنولوژی باید به ساختار و سیستم ما کمک کنه نه اینکه خودش bottleneck سیستم بشه، تکنولوژی جذابه ولی نکته‌ی مهم اینه که ما اول از همه به این مساله دقت کنیم که باید به عنوان برنامه نویس روی تکنولوژی سوار باشیم نه برعکس (بزرگترین مشکل وقتی پیش میاد که هیجان زده ابزارو تکنولوژی جدید آشنا بشیم و بخواهیم در سیستم استفاده کنیم! هیجان بیش از حد قاتل سسیستمه) با انتخاب اشتباهمون کل سازمان رو دچار مشکل نکنیم، مخصوصل وقتی با یک تکنولوژی آشنا میشیم وفور لایبری‌های مختلف نباید ما رو گمراه کنه!
4) وقتی دیدتون به عنوان توسعه دهنده از مونولوتیک خارج میشه و به مایکروسرویس نگاه میکنید حتما باید از زاویه‌ی خیلی بالاتری نگاه کنید، جایی که توقع میره همه‌ی سیستم بتونه به همدیگه به بهترین نحو بدون مشکل کار کنه پس انتخاب ابزار خیلی اهمیتش بالاتره از تکنولوژی جدید که شاید تازه هم باهاش آشنا شده باشید، api-gateway  به عنوان یک اصل برای معماری ماکروسرویس پذیرفته شده و حتی خیلی توصیه میشه
5) نگاه به سیستم‌های طراحی شده‌ی دنیا هم خیلی بهمون کمک میکنه حتی پروژه‌های بزرگی مثله etcd از gateway استفاده کرده اند چرا که سنگ بزرگ نشانه نزدنه و ریسک تغییرات خیلی بزرگ و بنیادین کل نرم افزار رو به چالش میشکه و علاوه بر اینکه هزینه‌ی زیاد learning curve برای کله تیم داره، ریسک شکست رو هم خیلی بالا میبره، شما فرض کنید که برنامه نویس فرانت اند که تا حالا تو عمرش به غیر از restful api چیز دیگه ای ندیده ناگهان با کلی ابزار جدید آشنا باید بشه که حتی ممکنه رسمی و صد در صد قابل اتکا هم نباشه، این خودش شروع fail شدن یک سیستم رو نتیجه میده!
در مقاله‌ی بعدی سعی میکنم مثال عملی با استفاده از زبان‌های برنامه نویسی مختلف بزنم و توضیحات جزئی‌تری بدم و موارد بیشتر رو فقط و فقط به خواننده که خودش قطعا توسعه دهنده هست و میتونه بالا و پایین رو ببینه میسپارم!
ممنون
مطالب
خروجی Excel با حجم بالا در برنامه‌های ‌ASP.NET Core با استفاده از MiniExcel

امکان خروجی اکسل از گزارشات سیستم، یکی از بایدهای بیشتر سیستم‌های اطلاعاتی می‌باشد؛ یکی از چالش‌های اصلی در تولید این نوع خروجی، افزایش مصرف حافظه متناسب با افزایش حجم دیتا می‌باشد. از آنجایی‌که بیشتر راهکارهای موجود از جمله ClosedXml یا Epplus کل ساختار را ابتدا تولید کرده و اصطلاحا خروجی مورد نظر را بافر می‌کنند، برای حجم بالای اطلاعات مناسب نخواهند بود. راهکار برای خروجی CSV به عنوان مثال خیلی سرراست می‌باشد و می‌توان با چند خط کد، به نتیجه دلخواه از طریق مکانیزم Streaming رسید؛ ولی ساختار Excel به سادگی فرمت CSV نیست و برای مثال فرمت Excel Workbook با پسوند xlsx یک بسته Zip شده‌ای از فایل‌های XML می‌باشد.

معرفی MiniExcel

MiniExcel یک کتابخانه سورس باز با هدف به حداقل رساندن مصرف حافظه در زمان پردازش فایل‌های Excel در دات نت می‌باشد. در مقایسه با Aspose از منظر امکانات شاید حرفی برای گفتن نداشته باشد، ولی از جهت خواندن اطلاعات فایل‌های Excel با قابلیت پشتیبانی از ‌LINQ و Deferred Execution در کنار مصرف کم حافظه و جلوگیری از مشکل OOM خیلی خوب عمل می‌کند. در تصویر زیر مشخص است که برای عمده عملیات پیاده‌سازی شده، از استریم‌ها بهره برده شده است.

همچنین در زیر مقایسه‌ای روی خروجی ۱ میلیون رکورد با تعداد ۱۰ ستون در هر ردیف انجام شده‌است که قابل توجه می‌باشد:

Logic : create a total of 10,000,000 "HelloWorld" excel
LibraryMethodMax Memory UsageMean
MiniExcel'MiniExcel Create Xlsx'15 MB11.53181 sec
Epplus'Epplus Create Xlsx'1,204 MB22.50971 sec
OpenXmlSdk'OpenXmlSdk Create Xlsx'2,621 MB42.47399 sec
ClosedXml'ClosedXml Create Xlsx'7,141 MB140.93992 sec

به شدت API خوش دستی برای استفاده دارد و شاید مطالعه سورس کد آن از جهت طراحی نیز درس آموزی داشته باشد. در ادامه چند مثال از مستندات آن را می‌توانید ملاحظه کنید:

var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");
MiniExcel.SaveAs(path, new[] {
    new { Column1 = "MiniExcel", Column2 = 1 },
    new { Column1 = "Github", Column2 = 2}
});

// DataReader export multiple sheets (recommand by Dapper ExecuteReader)

using (var cnn = Connection)
{
    cnn.Open();
    var sheets = new Dictionary<string,object>();
    sheets.Add("sheet1", cnn.ExecuteReader("select 1 id"));
    sheets.Add("sheet2", cnn.ExecuteReader("select 2 id"));
    MiniExcel.SaveAs("Demo.xlsx", sheets);
}

طراحی یک ActionResult سفارشی برای استفاده از MiniExcel

برای این منظور نیاز است تا Stream مربوط به Response درخواست جاری را در اختیار این کتابخانه قرار دهیم و از سمت دیگر دیتای مورد نیاز را به نحوی که بافر نشود و از طریق مکانیزم Streaming در EF (استفاده از Deferred Execution و Enumerableها) مهیا کنیم. برای امکان تعویض پذیری (این سناریو در پروژه واقعی و باتوجه به جهت وابستگی‌ها می‌تواند ضروری باشد) از دو واسط زیر استفاده خواهیم کرد:

public interface IExcelDocumentFactory
{
    ILargeExcelDocument CreateLargeDocument(IEnumerable<ExcelColumn> headers, Stream stream);
}


public interface ILargeExcelDocument : IAsyncDisposable, IDisposable
{
    Task Write<T>(
        PaginatedEnumerable<T> items,
        int count,
        int sizeLimit,
        CancellationToken cancellationToken = default) where T : notnull;
}

متد CreateLargeDocument یک وهله از ILargeExcelDocument را در اختیار مصرف کننده قرار می‌دهد که قابلیت نوشتن روی آن از طریق متد Write را خواهد داشت. روش واکشی دیتا از طریق Delegate تعریف شده با نام PaginatedEnumerable به مصرف کننده محول شده‌است که در ادامه امضای آن را می‌توانید مشاهده کنید:

public delegate IEnumerable<T> PaginatedEnumerable<out T>(int page, int pageSize);

در ادامه پیاده‌سازی واسط ILargeExcelDocument برای MiniExcel به شکل زیر خواهد بود:

internal sealed class MiniExcelDocument(Stream stream, IEnumerable<ExcelColumn> columns) : ILargeExcelDocument
{
    private const int SheetLimit = 1_048_576;
    private bool _disposedValue;

    public async Task Write<T>(
        PaginatedEnumerable<T> items,
        int count,
        int sizeLimit,
        CancellationToken cancellationToken = default)
        where T : notnull
    {
        ThrowIfDisposed();
        
        // TODO: apply sizeLimit
        var properties = FastReflection.Instance.GetProperties(typeof(T))
            .ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);

        var sheets = new Dictionary<string, object>();
        var index = 1;
        while (count > 0)
        {
            cancellationToken.ThrowIfCancellationRequested();

            IEnumerable<Dictionary<string, object>> reader = items(index, SheetLimit)
                .Select(item =>
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    return columns.ToDictionary(h => h.Title, h => ValueOf(item, h.Name, properties));
                });

            sheets.Add($"sheet_{index}", reader);
            count -= SheetLimit;
            index++;
        }

        // This part is forward-only, and we are pretty sure that streaming will happen without buffering.
        await stream.SaveAsAsync(sheets, cancellationToken: cancellationToken);
    }

    private void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                // TODO: dispose managed state (managed objects)
            }

            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
            // TODO: set large fields to null
            _disposedValue = true;
        }
    }

    ~MiniExcelDocument()
    {
        Dispose(disposing: false);
    }

    public void Dispose()
    {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync()
    {
        Dispose();
        await ValueTask.CompletedTask;
    }

    private void ThrowIfDisposed()
    {
        if (!_disposedValue) return;
        
        throw new ObjectDisposedException(nameof(MiniExcelDocument));
    }
    private static object ValueOf<T>(T record, string prop, IDictionary<string, FastPropertyInfo> properties)
        where T : notnull
    {
        var property = properties[prop] ??
                       throw new InvalidOperationException($"There is no property with given name [{prop}]");

        return NormalizeValue(property.GetValue?.Invoke(record));
    }

    private static object NormalizeValue(object? value)
    {
        if (value == null) return null!;

        return value switch
        {
            DateTime dateTime => dateTime.ToShortPersianDateTimeString(),
            TimeSpan time => time.ToString(@"hh\:mm\:ss"),
            DateOnly dateTime => dateTime.ToShortPersianDateString(false),
            TimeOnly time => time.ToString(@"hh\:mm\:ss"),
            bool boolean => boolean ? "بلی" : "خیر",
            IEnumerable<object> values => string.Join(',', values.Select(NormalizeValue).ToList()),
            Enum enumField => enumField.GetEnumStringValue(),
            _ => value
        };
    }
}

در بدنه متد Write باتوجه به تعداد کل رکوردها، یک کوئری برای هر شیت از طریق فراخوانی متد منتسب به پارامتر items اجرا خواهد شد؛ توجه کنید که اجرای این کوئری مشخصا به تعویق افتاده و تا زمان اولین MoveNext، اجرایی صورت نخواهد گرفت (مفهوم Deferred Execution). به این ترتیب باقی کارها از جمله فرمت کردن مقادیر در سمت برنامه و از طریق Linq To Object انجام خواهد شد. همچنین پیاده‌سازی Factory مرتبط با آن به شکل زیر خواهد بود:

internal sealed class ExcelDocumentFactory : IExcelDocumentFactory
{
    public ILargeExcelDocument CreateLargeDocument(IEnumerable<ExcelColumn> columns, Stream stream)
    {
        return new MiniExcelDocument(stream, columns);
    }
}

در ادامه ActionResult سفارشی برای گرفتن خروجی اکسل را به شکل زیر می توان پیاده‌سازی کرد:

public class ExcelExportResult<T>(PaginatedEnumerable<T> items, int count, ExportMetadata metadata) : ActionResult
    where T : notnull
{
    private const string ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    private const string Extension = ".xlsx";
    private const int SizeLimit = int.MaxValue;

    private readonly IReadOnlyList<FastPropertyInfo> _properties = FastReflection.Instance.GetProperties(typeof(T));

    public override async Task ExecuteResultAsync(ActionContext context)
    {
        var sp = context.HttpContext.RequestServices;
        var factory = sp.GetRequiredService<IExcelDocumentFactory>();

        var disposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment);
        disposition.SetHttpFileName(MakeFilename());

        context.HttpContext.Response.Headers[HeaderNames.ContentDisposition] = disposition.ToString();
        context.HttpContext.Response.Headers.Append(HeaderNames.ContentType, ContentType);
        context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;

        //TODO: deal with exception, because our global exception handling cannot take into account while the response is started.

        await using var bodyStream = context.HttpContext.Response.BodyWriter.AsStream();
        await context.HttpContext.Response.StartAsync(context.HttpContext.RequestAborted);
        await using (var document = factory.CreateLargeDocument(MakeColumns(), bodyStream))
        {
            await document.Write(items, count, SizeLimit, context.HttpContext.RequestAborted);
        }

        await context.HttpContext.Response.CompleteAsync();
    }

    private string MakeFilename()
    {
        return
            $"{metadata.Title} - {DateTime.UtcNow.ToEpochSeconds()}{Extension}";
    }

    private IEnumerable<ExcelColumn> MakeColumns()
    {
        var types = _properties.ToDictionary(p => p.Name, p => p.PropertyType, StringComparer.OrdinalIgnoreCase);
        return metadata.Fields.Select(f =>
        {
            var type = types[f.Name];

            type = Nullable.GetUnderlyingType(type) ?? type;

            if (type.IsEnum ||
                type == typeof(DateOnly) ||
                type == typeof(TimeOnly) ||
                type == typeof(bool) ||
                type == typeof(TimeSpan) ||
                type == typeof(DateTime))
            {
                type = typeof(string);
            }

            return new ExcelColumn(f.Name, f.Title, type);
        });
    }
}

در اینجا از طریق ExportMetadata که از سمت کاربر تعیین می‌شود، مشخص خواهد شد که کدام فیلدها در فایل نهایی حضور داشته باشند. در بدنه متد ExecuteResultAsync یکسری هدر مرتبط با کار با فایل‌ها تنظیم شده‌است و سپس از طریق BodyWriter و متد AsStream به استریم مورد نظر دست یافته و در اختیار متد Write مربوط به document ایجاد شده، قرار داده‌ایم. یک نمونه استفاده از آن برای موجودیت فرضی مشتری می تواند به شکل زیر باشد:

[ApiController, Route("api/customers")]
public class CustomersController(IDbContext dbContext) : ControllerBase
{
    [HttpGet("export")]
    public async Task<ActionResult> ExportCustomers([FromQuery] ExportMetadata metadata,
        CancellationToken cancellationToken)
    {
        var count = await dbContext.Set<Customer>().CountAsync(cancellationToken);
        return this.Export(
            (page, pageSize) => dbContext.Set<Customer>()
                .OrderBy(c => c.Id)
                .Skip((page - 1) * pageSize)
                .Take(pageSize)
                .AsNoTracking()
                .AsEnumerable(), // Enable streaming instead of buffering through deferred execution
            count,
            metadata);
    }
}

در اینجا از طریق Extension Method مهیا شده روش کوئری کردن برای هر شیت را مشخص کرده‌ایم؛ نکته مهم در ایجاد استفاده از ‌متد AsEnumerable می باشد که در عمل یک Type Casting انجام می دهد که باقی متدهای استفاده شده روی خروجی، از طریق Linq To Object اعمال شود و همچنین نیاز به استفاده از ToList و یا موارد مشابه را نخواهیم داشت. نمونه درخواست GET برای این API می تواند به شکل زیر باشد:

http://localhost:5118/api/customers/export?Title=Test&Fields[0].Name=FirstName&Fields[0].Title=First name&Fields[1].Name=LastName&Fields[1].Title=Last name&Fields[2].Name=BirthDate&Fields[2].Title=BirthDate

سورس کد مثال قابل اجرا از طریق مخزن زیر قابل دسترس می باشد:

https://github.com/rabbal/large-excel-streaming

در این مثال در زمان آغاز برنامه، ۱۰ میلیون رکورد در جدول Customer ثبت خواهد شد که در ادامه می توان از آن خروجی Excel تهیه کرد.

نکته مهم: توجه داشته باشید که استفاده از این روش قابلیت از سرگیری مجدد برای دانلود را نخواهد داشت و شاید بهتر است این فرآیند را از طریق یک Job انجام داده و با استفاده از قابلیت‌های Multipart Upload مربوط به یک BlobStroage مانند Minio، خروجی مورد نظر از قبل ذخیره کرده و لینک دانلودی را در اختیار کاربر قرار دهید.

مطالب
Dependency Injection در Asp.Net WebApi (روش اول)
طی این پست با تزریق وابستگی‌ها در Asp.net MVC آشنا شدید. روش ذکر شده در آن برای کنترلرهای Web Api جوابگو نیست و باید از روش‌های دیگری برای این منظور استفاده نماییم.

نکته 1: برای پیاده سازی این مثال‌ها، Castle Windsor به عنوان IOC Container انتخاب شده است. بدیهی است می‌توانید از Ioc Container مورد نظر خود نیز بهره ببرید.
نکته 2: می‌توانید از مقاله [هاست سرویس‌های Web Api با استفاده از OWIN و TopShelf] جهت هاست سرویس‌های web Api خود استفاده نمایید.

روش اول

اگر قبلا در این زمینه جستجو کرده باشید، به احتمال زیاد با مفهوم IDependencyResolver بیگانه نیستید. درباره استفاده از این روش مقالات متعددی نوشته شده است؛ حتی در مثال‌های موجود در خود سایت MSDN نیز این روش را مرسوم دانسته و آن را به اشتراک می‌گذارند. جهت نمونه می‌توانید این پروژه را دانلود کرده و کد‌های آن را بررسی کنید.
در این روش، قدم اول، ساخت یک کلاس و پیاده سازی اینترفیس IDependencyResolver می‌باشد؛ به صورت زیر:
public class ApiDependencyResolver : IDependencyResolver
    {
        public ApiDependencyResolver(IWindsorContainer container)
        {
            Container = container;
        }

        public IWindsorContainer Container
        {
            get;
            private set;
        }

        public object GetService(Type serviceType)
        {
            try
            {
                return Container.Kernel.HasComponent(serviceType) ? Container.Resolve(serviceType) : null;
            }
            catch (Kernel.ComponentNotFoundException)
            {
                return null;
            }
        }

        public IEnumerable<object> GetServices(Type serviceType)
        {
            try
            {
                return Container.ResolveAll(serviceType).Cast<object>();
            }
            catch (Kernel.ComponentNotFoundException)
            {
                return Enumerable.Empty<object>();
            }
        }
        public IDependencyScope BeginScope()
        {
            return new SharedDependencyResolver(Container);
        }

        public void Dispose()
        {
            Container.Dispose();
        }
    }
اینترفیس IDependencyResolver از اینترفیس دیگری به نام IDependencyScope ارث می‌برد که دارای دو متد اصلی به نام‌های GetService و GetServices است که جهت وهله سازی کنترلرها استفاده می‌شوند. با فراخوانی این متد‌ها، نمونه‌ی ساخته شده توسط Container بازگشت داده خواهد شد.


کاربرد متد BeginScope چیست
؟

کنترلر‌ها به صورت (Per Request) بر اساس هر درخواست وهله سازی خواهند شد. جهت مدیریت چرخه‌ی عمر کنترلرها و منابع در اختیار آن‌ها، از متد BeginScope استفاده می‌شود. به این صورت که نمونه‌ی اصلی DependencyResolver در هنگام شروع برنامه به GlobalConfiguration پروژه Attach خواهد شد. سپس به ازای هر درخواست، جهت وهله سازی Controller‌ها، متد GetService از محدوده داخلی (منظور فراخوانی متد BeginScope است) باعث ایجاد نمونه و بعد از اتمام فرآیند، متد Dispose باعث آزاد سازی منابع موجود خواهد شد.
پیاده سازی متد BeginScope وابسته به IocContainer مورد استفاده شما است. در این جا  کلاس SharedDependencyResolver را به صورت زیر پیاده سازی کردم:
    public class SharedDependencyResolver : IDependencyScope
    {
        public SharedDependencyResolver(IWindsorContainer container)
        {
            Container = container;
            Scope = Container.BeginScope();
        }

        public IWindsorContainer Container
        {
            get;
            private set;
        }

        public IDisposable Scope
        {
            get;
            private set;
        }

        public object GetService(Type serviceType)
        {
            try
            {
                return Container.Kernel.HasComponent(serviceType) ? Container.Resolve(serviceType) : null;
            }
            catch (ComponentNotFoundException)
            {
                return null;
            }
        }

        public IEnumerable<object> GetServices(Type serviceType)
        {
            try
            {
                return Container.ResolveAll(serviceType).Cast<object>();
            }
            catch (ComponentNotFoundException)
            {
                return null;
            }
        }

        public void Dispose()
        {
            Scope.Dispose();
        }
    }
اگر از UnityContainer استفاده می‌کنید کافیست تکه کد زیر را جایگزین کلاس بالا نمایید:
public IDependencyScope BeginScope()
{
    var child = container.CreateChildContainer();
    return new ApiDependencyResolver(child);
}
برای جستجوی خودکار کنترلرها و  رجیستر کردن آن‌ها به برنامه Windsor امکانات جالبی را در اختیار ما قرار می‌دهد. ابتدا یک Installer ایجاد می‌کنیم:
public class KernelInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {          
           container.Register(Classes.FromThisAssembly().BasedOn<ApiController>().LifestyleTransient());         
            container.Kernel.Resolver.AddSubResolver(new CollectionResolver(container.Kernel, true));
        }
    }

در پایان در کلاس Startup نیز کافیست مراحل زیر را انجام دهید:
 »ابتدا Installer نوشته شده را به WindsorContainer معرفی نمایید.
»DependencyResolver  نوشته شده را  به HttpConfiguration معرفی کنید.
»عملیات Routing مورد نظر را ایجاد و سپس config مورد نظر را در اختیار appBuilder قرار دهید.
public class Startup
    {
        public void Configuration( IAppBuilder appBuilder )
        {
           var container = new WindsorContainer();
            container.Install(new KernelInstaller());

            var config = new HttpConfiguration
            {
                 DependencyResolver = new ApiDependencyResolver(container)
            };                  

            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "Default" ,
                routeTemplate: "{controller}/{action}/{name}" ,
                defaults: new { name = RouteParameter.Optional }                
            );

            config.EnsureInitialized();          
         
            appBuilder.UseWebApi( config );
        }
    }
نکته: این روش به دلیل استفاده از الگوی ServiceLocator و همچنین نداشتن Context درخواست ها  روشی منسوخ شده می‌باشد که طی این مقاله جناب نصیری به صورت کامل به این مبحث پرداخته اند.

مطالب
تغییر نام دسته جمعی تعدادی فایل PDF بر اساس متادیتای فایل‌ها
فرض کنید تعداد زیادی فایل PDF را با اسامی نامفهومی داریم. برای نظم بخشیدن و یافتن ساده‌تر مطالب شاید بهتر باشد این فایل‌ها را بر اساس عنوان اصلی ذخیره شده در فایل، تغییر نام دهیم.


امکان خواندن meta data فوق (البته در صورت وجود)، توسط iTextSharp وجود دارد. در ادامه قطعه کد ساده‌ای را ملاحظه می‌کنید که در یک پوشه، تمام فایل‌های PDF را یافته و بر اساس Title یا Subject آن‌ها، فایل موجود را تغییر نام می‌دهد:
using System.IO;
using iTextSharp.text.pdf;

namespace BatchRename
{
    class Program
    {
        private static string getTitle(PdfReader reader)
        {
            string title;
            reader.Info.TryGetValue("Title", out title); // Reading PDF file's meta data
            return string.IsNullOrWhiteSpace(title) ? string.Empty : title.Trim();
        }

        private static string getSubject(PdfReader reader)
        {
            string subject;
            reader.Info.TryGetValue("Subject", out subject); // Reading PDF file's meta data
            return string.IsNullOrWhiteSpace(subject) ? string.Empty : subject.Trim();
        }

        static void Main(string[] args)
        {
            var dir = @"D:\Path";
            if (!dir.EndsWith(@"\"))
                dir = dir + @"\";

            foreach (var file in Directory.GetFiles(dir, "*.pdf"))
            {
                var reader = new PdfReader(file);
                var title = getTitle(reader);
                var subject = getSubject(reader);
                reader.Close();

                string newFile = string.Empty;
                if (!string.IsNullOrWhiteSpace(title))
                {
                    newFile = dir + title + ".pdf";
                }
                else if (!string.IsNullOrWhiteSpace(subject))
                {
                    newFile = dir + subject + ".pdf";
                }

                if (!string.IsNullOrWhiteSpace(newFile))
                    File.Move(file, newFile);
            }
        }
    }
}

در قطعه کد فوق علت مراجعه به reader.Info، بر اساس ساختار یک فایل PDF است. در Dictionary به نام Info (تصویر فوق)، در یک سری کلید مشخص، اطلاعاتی مانند تهیه کننده، عنوان و غیره درج می‌شوند. به این ترتیب با استفاده از شیء PdfReader، فایل را گشوده، این متادیتا را خوانده و سپس بر اساس آن می‌توان فایل را تغییر نام داد.
 
بازخوردهای دوره
نگاهی به انواع Aspects موجود در کتابخانه PostSharp
- بحث در مورد AOP بدون ذکر نامی از PostSharp بی‌معنا بود. به همین جهت چند قسمتی به آن اختصاص داده شد. حداقل از لحاظ بحث مفهومی ارزشمند است.
- در سازمان‌ها امکان تشکیل یک مخزن نیوگت محلی وجود دارد. یعنی فقط کافی است یکی از سیستم‌ها تبدیل به مخزن شود و بقیه از آن استفاده کنند. اطلاعات بیشتر در اینجا
- پیشنهاد من استفاده از پروژه‌های سورس باز مشابهی است مانند Fody. یک نمونه از کاربرد آن‌را در ادامه این دوره بررسی کرده‌ایم: «معرفی پروژه NotifyPropertyWeaver ». امکانات زیادی دارد. یا اینکه اصلا از IL Weaving استفاده نکنید و از dynamic proxy مطرح شده مانند پروژه castle core که در قسمت‌های قبل بررسی شد، استفاده نمائید.
- post sharp زمانیکه از طریق نیوگت نصب می‌شود، خودش را در سیستم build ویژوال استودیو مرتبط با پروژه جاری ثبت می‌کند. پس از اینکه dll یا فایل exe شما توسط VS.NET تولید شد، به صورت خودکار کار post sharp آغاز شده و کدهای IL اضافی پیاده سازی کننده aspects مدنظر را به اسمبلی‌های برنامه اضافه می‌کند.
بازخوردهای دوره
صفحات مودال در بوت استرپ 3
مثال MVC آن کامل هست (پیوست شده در انتهای بحث در پوشه‌ی mvc-sample آن). شامل فایل‌های js، css و مدل و کنترلر، به همراه Viewها و layout آن که تعاریف لازم در آن هست و فایل‌های Viewهای Home که نحوه‌ی استفاده از آن ذکر شده‌اند.
مطالب
مفاهیم پایه سیستم های کنترل نسخه؛ قسمت دوم : SVN
در قسمت قبلی، اهمیت استفاده از سیستم‌های کنترل نسخه را بیان کردیم و مفاهیم پایه‌ای گیت را مورد بررسی قرار دادیم. در این قسمت مفاهیم پایه‌ای SVN را مورد بررسی قرار می‌دهیم.
SVN مخفف عبارت SubVersion هست و یک سیستم کنترل نسخه‌ی رایگان و متن باز است که توسط شرکت  کلاب نت حمایت می‌شود. به تعدادی از این سیستم‌ها، سیستم‌های «مدیریت پیکربندی نرم افزار»   (Software Configuration Manager (SCM هم اطلاق می‌شود.
در این سیستم فایل‌ها در یک مخزن Repository مرکزی ذخیره می‌شوند و با هر تغییری که در فایل‌ها و دایرکتوری‌ها ایجاد می‌شود، آن‌ها را ثبت می‌کند. این امکان به ما این اجازه را می‌دهد که نسخه‌ی قدیمی فایل‌ها را بازیابی کرده و تاریخچه‌ی اینکه فایل‌ها چگونه و چه موقع و توسط چه کسی تغییر کرده‌اند، به ما نشان دهد. تصویر سلسله مراتبی زیر به خوبی نحوه ارتباط کلاینت‌ها را به این مخزن نشان می‌دهد.
SVN برای مدیریت چندین نسخه از فایل ها، از مدل «کپی، ویرایش، ادغام» Copy-Modify-Merge استفاده می‌کند. در این مدل که هر کاربری در مخزن عمل خواندن را انجام می‌دهد، یک کپی جداگانه و کاملا شخصی برای او گرفته شده و سپس کپی‌های شخصی خودش را ویرایش می‌کند. بعد از اینکه ویرایش تکمیل شد، کپی شخصی خودش را با یک فایل جدید و نهایی ادغام می‌کند.
این روش به شدت از روش «قفل کردن، ویرایش، آزادسازی» »Lock-Modify-Unlock کارآمدتر است و دیگر نیازی نیست که یک کاربر در یک زمان به این ساختار دسترسی داشته باشد و آن را ویرایش کند.

در تصویر بالا هری و سالی، یک کپی از مخزن موجود را گرفته و سپس هر کدام جداگانه بر روی کپی‌های خودشان مشغول به کار می‌شوند. سپس سالی کارش را زودتر به اتمام رسانده و مخزن را به روز می‌کند. بعد از آن، هری هم کارش به پایان می‌رسد و قصد به روز کردن مخزن را دارد ولی سیستم به او اجازه این کار را نمی‌دهد؛ چون این مخزن آن مخزن نیست که هری قبلا از آن کپی گرفته است. آن مخزن بعد از به روزرسانی سالی تغییر یافته است. پس او مجبور است تا تغییرات جدید مخزن را دریافت کرده و کپی خودش را به روز کند. پس از آن می‌تواند کپی خودش را بر روی مخزن اعمال کند (با فرض اینکه تغییرات جدید هیچ تصادمی با تغییراتی که روی کپی خودش اعمال کرده است ندارند).


سناریو بالا با احتساب وجود تصادم

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

هری درخواست ادغام آخرین تغییرات مخزن را با کپی خودش می‌کند. از آنجا که فایل A تصادم دارد یک فلگ flag از این وضعیت می‌گیرد. حال هری میتواند تفاوت‌های ایجاد شده را ببیند و بین آن‌ها یکی را انتخاب کند. در این وضعیت هری همپوشانی‌های کدها را برطرف می‌کند و شاید هم بحثی در مورد این تصادم با سالی داشته باشد تا بهترین تغییر کد انتخاب گردد و نهایتا به روشی کاملا امن و مطمئن، با مخزن اصلی ادغام می‌شود.

پی نوشت : نرم افزارها نمی‌توانند موضوع تصادم را به طور خودکار اعمال کنند. از آنجا که نیاز به تصمیم گیری و درک هوشمند دارد این کار به صورت انسانی باید بررسی گردد.

مطالب
مقدمه‌ای بر Docker، قسمت دوم
در قسمت قبلی با مفاهیم اولیه‌ی داکر آشنا شدیم و در این قسمت بیشتر به مباحث عملی آن خواهیم پرداخت. ضمن اینکه طریقه‌ی نصب داکر نیز بسیار ساده‌است و برای مطالعه‌ی بیشتر به سایت مرجع آن مراجعه بفرمایید (برای ویندوز، مک و لینوکس قسمت‌های مجزایی تعبیه شده و نصب آن آموزش داده شده‌است).
در قسمت قبلی با Volume آشنا شدیم؛ اکنون قصد داریم آن را اجرایی نموده و برنامه‌ی خود را بر روی آن اجرا نماییم. عملا با استفاده از Volume، قابلیت این را خواهید داشت که از Container ایجاد شده، با استفاده از volume، اشاره‌گری را به source code خود بر روی ماشین محلی داشته باشید و کد خود را بر روی آن اجرا نمایید.
مزایای استفاده از Volume
Volumeها بر روی Containerهای ویندوزی و لینوکسی اجرا میشوند.
قابلیت اشتراک گذاری volume بین container‌های مختلف وجود دارد.
Volumeها persisted هستند و حتی بعد از حذف container پایدار باقی می‌مانند و قابلیت استفاده‌ی مجدد را خواهند داشت.

ما طبق شکل فوق، می‌توانیم درون یک container، یک volume داشته باشیم و وقتی چیزی را درون آن می‌نویسیم، عملا داریم در قسمت خاصی به نام Docker Host عمل write کردن را انجام میدهیم که باعث میشود داکر متوجه آن شود. وقتی اسمی را به یک Volume انتساب می‌دهیم همانند /var/www، در واقع یک اسم مستعار (alias) می‌باشد که اشاره میکند به این Docker host موجود.
 
میخواهیم در این قسمت یک اپلیکیشن Nodejsِی را با استفاده از Volume بر روی Docker Container اجرا نماییم.
ابتدا دستور زیر را وارد کرده تا آخرین نسخه‌ی تصویر Nodejs را بر روی سیستم خود دریافت نمایید:
docker pull node
بعد از دریافت آن از طریق دستور زیر، لیست Image‌های cache شده بر روی سیستم خود را می‌توانید ببینید:
docker images
حال باید چنین چیزی را بر روی ترمینال خود مشاهده کنید:


در ادامه نیاز داریم یک دایرکتوری را ایجاد کرده و فایل index.js را درون آن بسازیم:

mkdir testapp
cd testapp
touch index.js
npm init
npm i express --save

یک دایرکتوری را ساختیم و همچنین express را نیز نصب نمودیم.

اکنون package.json را باز کرده و این قسمت را جایگزین کنید؛ تا با استفاده از npm start، برنامه اجرا شود:

"scripts": {
    "start": "node index"
  }

index.js را باز کرده و کد‌های زیر را وارد کنید:

const express = require('express')
const app = express()

const PORT = 3000;

app.get('/', function (req, res) {
  res.send('Hello World')
})

app.listen(PORT, function () {
  console.log(`listening on port ${PORT}!`)
})

همه چیز خیلی ساده در نظر گرفته شده است. از فریمورک express استفاده کردیم و یک سرور را بر روی پورت 3000 اجرا کرده و همچنین بر روی آدرس "/" یک سطر کد Hello World اجرا می‌شود.

خوب، فرض کنید قصد داریم با استفاده از volume، کد فوق را بر روی container اجرا کنیم.

برای اجرا شدن این کد‌ها بر روی Volume، دستور زیر را درون ترمینال خود وارد کنید:

docker run -d -p 3030:3000 -v $(pwd):/var/www -w "/var/www" node npm start

شرح دستور فوق:

دستور ساخت container با استفاده از ارگومان run

d- برای اجرا شدن container در حالت detached، باعث میشود اجرا شدن آن در حالت بک‌گراند بوده و بتوانید بر روی ترمینال مربوطه، دستورات دیگری را وارد نمایید.

p- پورت داخلی و خارجی را مشخص میکند. در اینجا پورت داخلی، 3000 و خارجی، 3030 میباشد.

آرگومان v- برای ساخت volume و (pwd)$ دایرکتوری جاری را بر روی سیستم شما، نشان خواهد داد. بعد از آن /var/www یک دایرکتوری فرضی است (هر آدرس دلخواهی را میتوانیم داشته باشیم) که قرار است بوسیله‌یdocker ساخته شود و از آن اشاره‌گری به دایرکتوری جاری بر روی ماشین محلی زده شود.

w- همان WorkingDirectory می‌باشد. بدلیل اینکه میخواهیم بر روی container به دایرکتوری که کد‌های ما وجود دارد، وارد شود.

بعد از آن اسم image ای را که قرار است از آن استفاده شود، آورده تا container ایجاد شود.

و بعد از npm start برنامه را اجرا خواهد کرد.

پس از اجرا کردن دستور فوق، container ایجاد میشود و قابلیت اجرایی دارد (با استفاده از ip و port خارجی بر روی browser میتوانیم برنامه را مشاهده کنیم).

حال با استفاده از دستور زیر لیست container‌های اجرایی را مشاهده خواهیم کرد:

docker ps

میبینید که container، اجرا شده و پورت آن مشخص شده‌است. تصویر و کلید هش شده‌ی منحصر به فرد آن را نیز مشاهده میفرمایید.

حتی میتوانید به راحتی درون container را با استفاده از دستور زیر مشاهده کنید:

docker exec -it 6003 bash

6003 ابتدای کلید container ایجاد شده‌است و با استفاده از bash وارد محیط command line در container ایجاد شده خواهیم شد و دسترسی کاملی خواهیم داشت.

بطور مثال برای دیدن کد‌های index.js بر روی container ایجاد شده، بعد از دستور فوق، command زیر را وارد نمایید:

cat index.js

 جالب است بدانید از آنجائیکه container از طریق volume به دایرکتوری محلی شما لینک شده‌است، به محض اینکه بر روی سیستم خود کدی را تغییر داده و دوباره دستور فوق را اجرا کنید، تغییرات را مشاهده خواهید کرد.

برای متوقف کردن container از دستور زیر باید استفاده کرد:

docker stop 6003

نکته: 6003 آی دی container است و برای اجرای مجدد آن docker start 6003

بعد از متوقف کردن container و اجرای دستور docker ps متوجه خواهید شد که دیگر Container در لیست containerهای باز نیست.

با استفاده از دستور زیر به لیست تمامی Container‌ها چه در حال اجرا و چه متوقف شده، دسترسی خواهیم داشت:

docker ps -a

برای حذف container نیز از دستور زیر استفاده میکنیم:

docker rm -v 6003

rm برای حذف container و همچنین v- برای حذف volume میباشد.