مطالب
خروجی 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، خروجی مورد نظر از قبل ذخیره کرده و لینک دانلودی را در اختیار کاربر قرار دهید.

مطالب
مدیریت درخواست‌های شرطی در ASP.NET MVC
فرض کنید کنید هدرهای کش کردن عناصر پویا و یا ثابت سایت را برای مدتی مشخص تنظیم کرده‌اید.
سؤال: مرورگر چه زمانی از کش محلی خودش استفاده خواهد کرد (بدون ارسال درخواستی به سرور) و چه زمانی مجددا از سرور درخواست دریافت مجدد این عنصر کش شده را می‌کند؟
برای پاسخ دادن به این سؤال نیاز است با مفهومی به نام Conditional Requests (درخواست‌های شرطی) آشنا شد که در ادامه به بررسی آن خواهیم پرداخت.


درخواست‌های شرطی

مرورگرهای وب دو نوع درخواست شرطی و غیر شرطی را توسط پروتکل HTTP و HTTPS ارسال می‌کنند. دراینجا، زمانی یک درخواست غیرشرطی ارسال می‌شود که نسخه‌ی ذخیره شده‌ی محلی منبع مورد نظر، مهیا نباشد. در این حالت، اگر منبع درخواستی در سرور موجود باشد، در پاسخ ارسالی خود وضعیت 200 یا HTTP/200 OK را باز می‌گرداند. اگر هدرهای دیگری نیز مانند کش کردن منبع در اینجا تنظیم شده باشند، مرورگر نتیجه‌ی دریافتی را برای استفاده‌ی بعدی ذخیره خواهد کرد.
در بار دومی که منبع مفروضی درخواست می‌گردد، مرورگر ابتدا به کش محلی خود نگاه خواهد کرد. همچنین در این حالت نیاز دارد که بداند این کش معتبر است یا خیر؟ برای بررسی این مورد ابتدا هدرهای ذخیره شده به همراه منبع، بررسی می‌شوند. پس از این بررسی اگر مرورگر به این نتیجه برسد که کش محلی معتبر است، دیگر درخواستی را به سرور ارسال نخواهد کرد.
اما در آینده اگر مدت زمان کش شدن تنظیم شده توسط هدرهای مرتبط، منقضی شده باشد (برای مثال با توجه به max-age هدر کش شدن منبع)، مرورگر هنوز هم درخواست کاملی را برای دریافت نسخه‌ی جدید منبع مورد نیاز، به سرور ارسال نمی‌کند. در اینجا ابتدا یک conditional request را به وب سرور ارسال می‌کند (یک درخواست شرطی). این درخواست شرطی تنها دارای هدرهای If-Modified-Since و یا If-None-Match است و هدف از آن سؤال پرسیدن از وب سرور است که آیا این منبع خاص، در سمت سرور اخیرا تغییر کرده‌است یا خیر؟ اگر پاسخ سرور خیر باشد، باز هم از همان کش محلی استفاده خواهد شد و مجددا درخواست کاملی برای دریافت نمونه‌ی جدیدتر منبع مورد نیاز، به سرور ارسال نمی‌گردد.
پاسخی که سرور جهت مشخص سازی عدم تغییر منابع خود ارسال می‌کند، با هدر HTTP/304 Not Modified مشخص می‌گردد (این پاسخ هیچ body خاصی نداشته و فقط یک سری هدر است). اما اگر منبع درخواستی اخیرا تغییر کرده باشد، پاسخ HTTP/200 OK را در هدر بازگشت داده شده، به مرورگر بازخواهد گرداند (یعنی محتوا را مجددا دریافت کن).


چه زمانی مرورگر درخواست‌های شرطی If-Modified-Since را به سرور ارسال می‌کند؟

اگر یکی از شرایط ذیل برقرار باشد، مرورگر حتی اگر تاریخ کش شدن منبع ویژه‌ای به 10 سال بعد تنظیم شده باشد، مجددا یک درخواست شرطی را برای بررسی اعتبار کش محلی خود به سرور ارسال می‌کند:
الف) کش شدن بر اساس هدر خاصی به نام vary صورت گرفته‌است (برای مثال بر اساس id یا نام یک فایل).
ب) اگر نحوه‌ی هدایت به صفحه‌ی جاری از طریق META REFRESH باشد.
ج) اگر از طریق کدهای جاوا اسکریپتی، دستور reload صفحه صادر شود.
د) اگر کاربر دکمه‌ی refresh را فشار دهد.
ه) اگر قسمتی از صفحه توسط پروتکل HTTP و قسمتی دیگر از آن توسط پروتکل HTTPS ارائه شود.
و ... اگر بر اساس هدر تاریخ مدت زمان کش شدن منبع، زمان منقضی شدن آن فرا رسیده باشد.


مدیریت درخواست‌های شرطی در ASP.NET MVC

تا اینجا به این نکته رسیدیم که قرار دادن ویژگی Output cache بر روی یک اکشن متد، الزاما به معنای کش شدن آن تا مدت زمان تعیین شده نخواهد بود و مرورگر ممکن است (در یکی از 6 حالت ذکر شده فوق) توسط ارسال هدر If-Modified-Since ، سعی در تعیین اعتبار کش محلی خود کند و اگر پاسخ 304 را از سرور دریافت نکند، حتما نسبت به دریافت مجدد و کامل آن منبع اقدام خواهد کرد.
سؤال: چگونه می‌توان هدر If-Modified-Since را در ASP.NET MVC مدیریت کرد؟
پاسخ: اگر از فیلتر OutputCache استفاده می‌کنید، به صورت خودکار هدر Last-Modified را اضافه می‌کند؛ اما این مورد کافی نیست.
در ادامه یک کنترلر و اکشن متد GetImage آن‌را ملاحظه می‌کنید که تصویری را از مسیر app_data/images خوانده و بازگشت می‌دهد. همچنین این تصویر بازگشت داده شده را نیز با توجه به OutputCache آن به مدت یک ماه کش می‌کند.
using System.IO;
using System.Web.Mvc;

namespace MVC4Basic.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        const int AMonth = 30 * 86400;

        [OutputCache(Duration = AMonth, VaryByParam = "name")]
        public ActionResult GetImage(string name)
        {
            name = Path.GetFileName(name);
            var path = Server.MapPath(string.Format("~/app_data/images/{0}", name));
            var content = System.IO.File.ReadAllBytes(path);
            return File(content, "image/png", name);
        }
    }
}
با این View که تصویر خود را توسط اکشن متد GetImage تهیه می‌کند:
 <img src="@Url.Action("GetImage","Home", new { name = "test.png"})"/>
در سطر اول متد GetImage، یک break point قرار دهید و سپس برنامه را توسط VS.NET اجرا کنید.
بار اول که صفحه‌ی اول برنامه درخواست می‌شود، یک چنین هدرهایی رد و بدل خواهند شد (توسط ابزار‌های توکار مرورگر وب کروم تهیه شده‌‌است؛ همان دکمه‌ی F12 معروف):
 Remote Address:127.0.0.1:5656
Request URL:http://localhost:10419/Home/GetImage?name=test.png
Request Method:GET
Status Code:200 OK

Response Headers
Cache-Control:public, max-age=2591916
Expires:Sat, 31 May 2014 12:45:55 GMT
Last-Modified:Thu, 01 May 2014 12:45:55 GMT
چون status code آن مساوی 200 است، بنابراین دریافت کامل فایل صورت خواهد گرفت. فیلتر OutputCache نیز مواردی مانند Cache-Control، Expires و Last-Modified را اضافه کرده‌است.

در همین حال اگر صفحه را ریفرش کنیم (فشردن دکمه‌ی F5)، اینبار هدرهای حاصل چنین شکلی را پیدا می‌کنند:
 Remote Address:127.0.0.1:5656
Request URL:http://localhost:10419/Home/GetImage?name=test.png
Request Method:GET
Status Code:304 Not Modified

Request Headers
If-Modified-Since:Thu, 01 May 2014 12:45:55 GMT
در اینجا چون یکی از حالات صدور درخواست‌های شرطی (ریفرش صفحه) رخداده است، هدر If-Modified-Since نیز در درخواست حضور دارد. پاسخ آن از طرف وب سرور (و نه برنامه؛ چون اصلا متد کش شده‌ی GetImage دیگر اجرا نخواهد شد و به break point داخل آن نخواهیم رسید)، 304 یا تغییر نکرده‌است. بنابراین مرورگر مجددا درخواست دریافت کامل فایل را نخواهد داد.

در ادامه بجای اینکه صفحه را ریفرش کنیم، یکبار دیگر در نوار آدرس آن، دکمه‌ی Enter را فشار خواهیم داد تا آدرس موجود در آن (ریشه سایت) مجددا در حالت معمولی دریافت شود.
 Remote Address:127.0.0.1:5656
Request URL:http://localhost:10419/Home/GetImage?name=test.png
Request Method:GET
Status Code:200 OK (from cache)
همانطور که ملاحظه می‌کنید اینبار پاسخ نمایش داده شده 200 است اما در ادامه‌ی آن ذکر شده‌است from cache. یعنی درخواستی را به سرور برای دریافت فایل ارسال نکرده است. عدم رسیدن به break point داخل متد GetImage نیز مؤید آن است.

مشکل! مرورگر را ببندید، تا کار دیباگ برنامه خاتمه یابد. مجددا برنامه را اجرا کنید. مشاهده خواهید کرد که ... اجرای برنامه در Break point قرار گرفته در سطر اول متد GetImage متوقف می‌شود. چرا؟! مگر قرار نبود تا یک ماه دیگر کش شود؟! هدر رد و بدل شده نیز Status Code:200 OK کامل است (که سبب دریافت کامل فایل می‌شود).
 Remote Address:127.0.0.1:5656
Request URL:http://localhost:10419/Home/GetImage?name=test.png
Request Method:GET
Status Code:200 OK

Request Headers
If-Modified-Since:Thu, 01 May 2014 12:45:55 GMT
راه حل: هدر If-Modified-Since را باید برای اولین بار فراخوانی اکشن متدی که حاصل آن نیاز است کش شود، خودمان و به صورت دستی مدیریت کنیم (فیلتر OutputCache این‌کار را انجام نمی‌دهد). به نحو ذیل:
using System;
using System.IO;
using System.Net;
using System.Web.Mvc;

namespace MVC4Basic.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        const int AMonth = 30 * 86400;

        [OutputCache(Duration = AMonth, VaryByParam = "name")]
        public ActionResult GetImage(string name)
        {
            name = Path.GetFileName(name);
            var path = Server.MapPath(string.Format("~/app_data/images/{0}", name));

            var lastWriteTime = System.IO.File.GetLastWriteTime(path);
            this.Response.Cache.SetLastModified(lastWriteTime.ToUniversalTime());

            var header = this.Request.Headers["If-Modified-Since"];
            if (!string.IsNullOrWhiteSpace(header))
            {
                DateTime isModifiedSince;
                if (DateTime.TryParse(header, out isModifiedSince) && isModifiedSince > lastWriteTime)
                {
                    return new HttpStatusCodeResult(HttpStatusCode.NotModified);
                }
            }

            var content = System.IO.File.ReadAllBytes(path);
            return File(content, "image/png", name);
        }
    }
}
در این حالت اگر مرورگر هدر If-Modified-Since را ارسال کرد، یعنی آدرس درخواستی هم اکنون در کش آن موجود است؛ فقط نیاز دارد تا شما پاسخ دهید که آیا آخرین تاریخ تغییر فایل درخواستی، از زمان آخرین درخواست صورت گرفته از سایت شما، تغییری کرده‌است یا خیر؟ اگر خیر، فقط کافی است 304 یا HttpStatusCode.NotModified را بازگشت دهید (بدون نیاز به بازگشت اصل فایل).
برای امتحان آن همانطور که عنوان شد فقط کافی است یکبار مرورگر خود را کاملا بسته و مجددا برنامه را اجرا کنید.
 Remote Address:127.0.0.1:5656
Request URL:http://localhost:10419/Home/GetImage?name=test.png
Request Method:GET
Status Code:304 Not Modified

Request Headers
If-Modified-Since:Thu, 01 May 2014 13:43:32 GMT

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


تبدیل این کدها به روش سازگار با ASP.NET MVC

ما در اینجا رسیدیم به یک سری کد تکراری if و else که باید در هر اکشن متدی که OutputCache دارد، تکرار شود. روش AOP وار آن در ASP.NET MVC، تبدیل این کدها به یک فیلتر با قابلیت استفاده‌ی مجدد است:
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public sealed class SetIfModifiedSinceAttribute : ActionFilterAttribute
    {
        public string Parameter { set; get; }
        public string BasePath { set; get; }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var response = filterContext.RequestContext.HttpContext.Response;
            var request = filterContext.RequestContext.HttpContext.Request;

            var path = getPath(filterContext);
            if (string.IsNullOrWhiteSpace(path))
            {
                response.StatusCode = (int)HttpStatusCode.NotFound;
                filterContext.Result = new EmptyResult();
                return;
            }

            var lastWriteTime = File.GetLastWriteTime(path);
            response.Cache.SetLastModified(lastWriteTime.ToUniversalTime());

            var header = request.Headers["If-Modified-Since"];
            if (string.IsNullOrWhiteSpace(header)) return;
            DateTime isModifiedSince;
            if (DateTime.TryParse(header, out isModifiedSince) && isModifiedSince > lastWriteTime)
            {
                response.StatusCode = (int)HttpStatusCode.NotModified;
                response.SuppressContent = true;
                filterContext.Result = new EmptyResult();
            }
        }

        string getPath(ActionExecutingContext filterContext)
        {
            if (!filterContext.ActionParameters.ContainsKey(Parameter)) return string.Empty;
            var name = filterContext.ActionParameters[Parameter] as string;
            if (string.IsNullOrWhiteSpace(name)) return string.Empty;

            var path = Path.GetFileName(name);
            path = filterContext.HttpContext.Server.MapPath(string.Format("{0}/{1}", BasePath, path));
            return !File.Exists(path) ? string.Empty : path;
        }
    }
در اینجا توسط filterContext، می‌توان به مقادیر پارامترهای ارسالی به یک اکشن متد، توسط filterContext.ActionParameters دسترسی پیدا کرد. بر این اساس می‌توان مقدار پارامتر نام فایل درخواستی را یافت. سپس مسیر کامل آن‌را بازگشت داد. اگر فایل موجود باشد، هدر If-Modified-Since درخواست، استخراج می‌شود. اگر این هدر تنظیم شده باشد، آنگاه بررسی خواهد شد که تاریخ تغییر فایل درخواستی جدیدتر است یا قدیمی‌تر از آخرین بار مرور سایت توسط مرورگر.

و برای استفاده از آن خواهیم داشت:
        [SetIfModifiedSince(Parameter = "name", BasePath = "~/app_data/images/")]
        [OutputCache(Duration = AMonth, VaryByParam = "name")]
        public ActionResult GetImage(string name)
        {
            name = Path.GetFileName(name);
            var path = Server.MapPath(string.Format("~/app_data/images/{0}", name));
            var content = System.IO.File.ReadAllBytes(path);
            return File(content, "image/png", name);
        }
البته بدیهی است اگر منطق ارسال 304 بر اساس تاریخ تغییر فایل باشد، روش فوق جواب خواهد داد. برای مثال اگر این منطق بر اساس تاریخ ثبت شده در دیتابیس است، قسمت محاسبه‌ی lastWriteTime را باید مطابق روش مطلوب خود تغییر دهید.


خلاصه‌ی بحث
چون فیلتر OutputCache در ASP.NET MVC، هدر If-Modified-Since را پردازش نمی‌کند (از این جهت که پردازش آن برای نمونه در مثال فوق وابسته به منطق خاصی است و عمومی نیست)، اگر با هر بار گشودن سایت خود مشاهده کردید، تصاویر پویایی که قرار بوده یک ماه کش شوند، دوباره از سرور درخواست می‌شوند (البته به ازای هرباری که مرورگر از نو اجرا می‌شود و نه در دفعات بعدی که صفحات سایت با همان وهله‌ی ابتدایی مرور خواهند شد)، نیاز است خودتان دسترسی کار پردازش هدر If-Modified-Since را انجام داده و سپس status code 304 را در صورت نیاز، ارسال کنید.
و در حالت عمومی، طراحی سیستم caching محتوای پویای شما بدون پردازش هدر If-Modified-Since ناقص است (تفاوتی نمی‌کند که از کدام فناوری سمت سرور استفاده می‌کنید).
 

برای مطالعه بیشتر
Understanding Conditional Requests and Refresh
Use If-Modified-Since header in ASP.NET 
Make your browser cache the output of an HttpHandler
304 Your images from a database
Conditional GET
Website Performance with ASP.NET - Part4 - Use Cache Headers
ASP.NET MVC 304 Not Modified Filter for Syndication Content
مطالب
ASP.NET MVC #5

بررسی نحوه انتقال اطلاعات از یک کنترلر به View‌های مرتبط با آن

در ASP.NET Web forms در فایل code behind یک فرم مثلا می‌توان نوشت Label1.Text و سپس مقداری را به آن انتساب داد. اما اینجا به چه ترتیبی می‌توان شبیه به این نوع عملیات را انجام داد؟ با توجه به اینکه در کنترلر‌ها هیچ نوع ارجاع مستقیمی به اشیاء رابط کاربری وجود ندارد و این دو از هم مجزا شده‌اند.
در پاسخ به این سؤال، همان مثال ساده قسمت قبل را ادامه می‌دهیم. یک پروژه جدید خالی ایجاد شده است به همراه HomeController ایی که به آن اضافه کرده‌ایم. همچنین مطابق روشی که ذکر شد، View ایی به نام Index را نیز به آن اضافه کرده‌ایم. سپس برای ارسال اطلاعات از یک کنترلر به View از یکی از روش‌های زیر می‌توان استفاده کرد:

الف) استفاده از اشیاء پویا

ViewBag یک شیء dynamic است که در دات نت 4 امکان تعریف آن میسر شده است. به این معنا که هر نوع خاصیت دلخواهی را می‌توان به این شیء انتساب داد و سپس این اطلاعات در View نیز قابل دسترسی و استخراج خواهد بود. مثلا اگر در اینجا به شیء ViewBag، خاصیت دلخواه Country را اضافه کنیم و سپس مقداری را نیز به آن انتساب دهیم:

using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Country = "Iran";
return View();
}
}
}

این اطلاعات در View مرتبط با اکشنی به نام Index به نحو زیر قابل بازیابی خواهد بود (نحوه اضافه کردن View متناظر با یک اکشن یا متد را هم در قسمت قبل با تصویر مرور کردیم):

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<p>
Country : @ViewBag.Country
</p>

در این مثال، @ در View engine جاری که Razor نام دارد، به این معنا می‌باشد که این مقداری است که می‌خواهم دریافت کنی (ViewBag.Country) و سپس آن‌را در حین پردازش صفحه نمایش دهی.


ب) انتقال اطلاعات یک شیء کامل و غیر پویا به View

هر پروژه جدید MVC به همراه پوشه‌ای به نام Models است که در آن می‌توان تعاریف اشیاء تجاری برنامه را قرار داد. در پروژه جاری، یک کلاس ساده را به نام Employee به این پوشه اضافه می‌کنیم:

namespace MvcApplication1.Models
{
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
}

اکنون برای نمونه یک وهله از این شیء را در متد Index ایجاد کرده و سپس به view متناظر با آن ارسال می‌کنیم (در قسمت return View کد زیر مشخص است). بدیهی است این وهله سازی در عمل می‌تواند از طریق دسترسی به یک بانک اطلاعاتی یا یک وب سرویس و غیره باشد.

using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Country = "Iran";

var employee = new Employee
{
Email = "name@site.com",
FirstName = "Vahid",
LastName = "N."
};

return View(employee);
}
}
}

امضاهای متفاوت (overloads) متد کمکی View هم به شرح زیر هستند:

ViewResult View(Object model)
ViewResult View(string viewName, Object model)
ViewResult View(string viewName, string masterName, Object model)


اکنون برای دسترسی به اطلاعات این شیء employee در View متناظر با این متد، چندین روش وجود دارد:

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div>
Country: @ViewBag.Country <‪br />
FirstName: @Model.FirstName
</div>

می‌توان از طریق شیء استاندارد دیگری به نام Model (که این هم یک شیء dynamic است مانند ViewBag قسمت قبل)، به خواص شیء یا مدل ارسالی به View جاری دسترسی پیدا کرد که یک نمونه از آن‌را در اینجا ملاحظه می‌کنید.
روش دوم، بر اساس تعریف صریح نوع مدل است به نحو زیر:

@model MvcApplication1.Models.Employee
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div>
Country: @ViewBag.Country
<‪br />
FirstName: @Model.FirstName
</div>

در اینجا در مقایسه با قبل، تنها یک سطر به اول فایل View اضافه شده است که در آن نوع شیء Model تعیین می‌گردد (کلمه model هم در اینجا با حروف کوچک شروع شده است). به این ترتیب اینبار اگر سعی کنیم به خواص این شیء دسترسی پیدا کنیم، Intellisense ویژوال استودیو ظاهر می‌شود. به این معنا که شیء Model بکارگرفته شده اینبار دیگر dynamic نیست و دقیقا می‌داند که چه خواصی را باید پیش از اجرای برنامه در اختیار استفاده کننده قرار دهد.
به این روش، روش Strongly typed view هم گفته می‌شود؛ چون View دقیقا می‌داند که چون نوعی را باید انتظار داشته باشد؛ تحت نظر کامپایلر قرار گرفته و همچنین Intellisense نیز برای آن مهیا خواهد بود.
به همین جهت این روش Strongly typed view، در بین تمام روش‌های مهیا، به عنوان روش توصیه شده و مرجح مطرح است.
به علاوه استفاده از Strongly typed views یک مزیت دیگر را هم به همراه دارد: فعال شدن یک code generator توکار در VS.NET به نام scaffolding. یک مثال ساده:
تا اینجا ما اطلاعات یک کارمند را نمایش دادیم. اگر بخواهیم یک لیست از کارمندها را نمایش دهیم چه باید کرد؟
روش کار با قبل تفاوتی نمی‌کند. اینبار در return View ما، یک شیء لیستی ارائه خواهد شد. در سمت View هم با یک حلقه foreach کار نمایش این اطلاعات صورت خواهد گرفت. راه ساده‌تری هم هست. اجازه دهیم تا خود VS.NET، کدهای مرتبط را برای ما تولید کند.
یک کلاس دیگر به پوشه مدل‌های برنامه اضافه کنید به نام Employees با محتوای زیر:

using System.Collections.Generic;

namespace MvcApplication1.Models
{
public class Employees
{
public IList<Employee> CreateEmployees()
{
return new[]
{
new Employee { Email = "name1@site.com", FirstName = "name1", LastName = "LastName1" },
new Employee { Email = "name2@site.com", FirstName = "name2", LastName = "LastName2" },
new Employee { Email = "name3@site.com", FirstName = "name3", LastName = "LastName3" }
};
}
}
}

سپس متد جدید زیر را به کنترلر Home اضافه کنید.

public ActionResult List()
{
var employeesList = new Employees().CreateEmployees();
return View(employeesList);
}

برای اضافه کردن View متناظر با آن، روی نام متد کلیک راست کرده و گزینه Add view را انتخاب کنید. در صفحه ظاهر شده:


تیک مربوط به Create a strongly typed view را قرار دهید. سپس در قسمت Model class، کلاس Employee را انتخاب کنید (نه Employees جدید را، چون از آن می‌خواهیم به عنوان منبع داده لیست تولیدی استفاده کنیم). اگر این کلاس را مشاهده نمی‌کنید، به این معنا است که هنوز برنامه را یکبار کامپایل نکرده‌اید تا VS.NET بتواند با اعمال Reflection بر روی اسمبلی برنامه آن‌را پیدا کند. سپس در قسمت Scaffold template گزینه List را انتخاب کنید تا Code generator توکار VS.NET فعال شود. اکنون بر روی دکمه Add کلیک نمائید تا View نهایی تولید شود. برای مشاهده نتیجه نهایی مسیر http://localhost/Home/List باید بررسی گردد.


ج) استفاده از ViewDataDictionary

ViewDataDictionary از نوع IDictionary با کلیدی رشته‌ای و مقداری از نوع object است. توسط آن شیء‌ایی به نام ViewData در ASP.NET MVC به نحو زیر تعریف شده است:

public ViewDataDictionary ViewData { get; set; }

این روش در نگارش‌های اولیه ASP.NET MVC بیشتر مرسوم بود. برای مثال:

using System;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewData["DateTime"] = "<‪br/>" + DateTime.Now;
return View();
}
}
}

و سپس جهت استفاده از این ViewData تعریف شده با کلید دلخواهی به نام DateTime در View متناظر با اکشن Index خواهیم داشت:

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div>
DateTime: @ViewData["DateTime"]
</div>

یک نکته امنیتی:
اگر به مقدار انتساب داده شده به شیء ViewDataDictionary دقت کنید، یک تگ br هم به آن اضافه شده است. برنامه را یکبار اجرا کنید. مشاهده خواهید کرد که این تگ به همین نحو نمایش داده می‌شود و نه به صورت یک سطر جدید HTML . چرا؟ چون Razor به صورت پیش فرض اطلاعات را encode شده (فراخوانی متد Html.Encode در پشت صحنه به صورت خودکار) در صفحه نمایش می‌دهد و این مساله از لحاظ امنیتی بسیار عالی است؛ زیرا جلوی بسیاری از حملات cross site scripting یا XSS را خواهد گرفت.
احتمالا الان این سؤال پیش خواهد آمد که اگر «عالمانه» بخواهیم این رفتار نیکوی پیش فرض را غیرفعال کنیم چه باید کرد؟
برای این منظور می‌توان نوشت:
@Html.Raw(myString)

و یا:
<div>@MvcHtmlString.Create("<h1>HTML</h1>")</div>

به این ترتیب خروجی Razor دیگر encode شده نخواهد بود.


د) استفاده از TempData

TempData نیز یک dictionary دیگر برای ذخیره سازی اطلاعات است و به نحو زیر در فریم ورک تعریف شده است:

public TempDataDictionary TempData { get; set; }

TempData در پشت صحنه از سشن‌های ASP.NET جهت ذخیره سازی اطلاعات استفاده می‌کند. بنابراین اطلاعات آن در سایر کنترلرها و View ها نیز در دسترس خواهد بود. البته TempData یک سری تفاوت هم با سشن معمولی ASP.NET دارد:
- بلافاصله پس از خوانده شدن، حذف خواهد شد.
- پس از پایان درخواست از بین خواهد رفت.
هر دو مورد هم به جهت بالابردن کارآیی برنامه‌های ASP.NET MVC و مصرف کمتر حافظه سرور درنظر گرفته‌ شده‌اند.
البته کسانی که برای بار اول هست با ASP.NET مواجه می‌شوند، شاید سؤال بپرسند این مسایل چه اهمیتی دارد؟ پروتکل HTTP، ذاتا یک پروتکل «بدون حالت» است یا Stateless هم به آن گفته می‌شود. به این معنا که پس از ارائه یک صفحه وب توسط سرور، تمام اشیاء مرتبط با آن در سمت سرور تخریب خواهند شد. این مورد متفاوت‌ است با برنامه‌های معمولی دسکتاپ که طول عمر یک شیء معمولی تعریف شده در سطح فرم به صورت یک فیلد، تا زمان باز بودن آن فرم، تعیین می‌گردد و به صورت خودکار از حافظه حذف نمی‌شود. این مساله دقیقا مشکل تمام تازه واردها به دنیای وب است که چرا اشیاء ما نیست و نابود شدند. در اینجا وب سرور قرار است به هزاران درخواست رسیده پاسخ دهد. اگر قرار باشد تمام این اشیاء را در سمت سرور نگهداری کند، خیلی زود با اتمام منابع مواجه می‌گردد. اما واقعیت این است که نیاز است یک سری از اطلاعات را در حافظه نگه داشت. به همین منظور یکی از چندین روش مدیریت حالت در ASP.NET استفاده از سشن‌ها است که در اینجا به نحو بسیار مطلوبی، با سربار حداقل توسط TempData مدیریت شده است.
یک مثال کاربردی در این زمینه:
فرض کنید در متد جاری کنترلر، ابتدا بررسی می‌کنیم که آیا ورودی دریافتی معتبر است یا خیر. در غیراینصورت، کاربر را به یک View دیگر از طریق کنترلری دیگر جهت نمایش خطاها هدایت خواهیم کرد.
همین «هدایت مرورگر به یک View دیگر» یعنی پاک شدن و تخریب اطلاعات کنترلر قبلی به صورت خودکار. بنابراین نیاز است این اطلاعات را در TempData قرار دهیم تا در کنترلری دیگر قابل استفاده باشد:

using System;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult InsertData(string name)
{
// Check for input errors.
if (string.IsNullOrWhiteSpace(name))
{
TempData["error"] = "name is required.";
return RedirectToAction("ShowError");
}
// No errors
// ...
return View();
}

public ActionResult ShowError()
{
var error = TempData["error"] as string;
if (!string.IsNullOrWhiteSpace(error))
{
ViewBag.Error = error;
}
return View();
}
}
}

در همان HomeController دو متد جدید به نام‌های InsertData و ShowError اضافه شده‌اند. در متد InsertData ابتدا بررسی می‌شود که آیا نامی وارد شده است یا خیر. اگر خیر توسط متد RedirectToAction، کاربر به اکشن یا متد ShowError هدایت خواهد شد.
برای انتقال اطلاعات خطایی که می‌خواهیم در حین این Redirect نمایش دهیم نیز از TempData استفاده شده است.
بدیهی است برای اجرا این مثال نیاز است دو View جدید برای متدهای InsertData و ShowError ایجاد شوند (کلیک راست روی نام متد و انتخاب گزینه Add view برای اضافه کردن View مرتبط با آن اکشن).
محتوای View مرتبط با متد افزودن اطلاعات فعلا مهم نیست، ولی View نمایش خطاها در ساده‌ترین حالت مثلا می‌تواند به صورت زیر باشد:

@{
ViewBag.Title = "ShowError";
}

<h2>Error</h2>

@ViewBag.Error

برای آزمایش برنامه هم مطابق مسیریابی پیش فرض و با توجه به قرار داشتن در کنترلری به نام Home، مسیر http://localhost/Home/InsertData ابتدا باید بررسی شود. چون آرگومانی وارد نشده، بلافاصله صفحه به آدرس http://localhost/Home/ShowError به صورت خودکار هدایت خواهد شد.


نکته‌ای تکمیلی در مورد Strongly typed viewها:
عنوان شد که Strongly typed view روش مرجح بوده و بهتر است از آن استفاده شود، زیرا اطلاعات اشیاء و خواص تعریف شده در یک View تحت نظر کامپایلر قرار می‌گیرند که بسیار عالی است. یعنی اگر در View بنویسم FirstName: @Model.FirstName1 چون FirstName1 وجود خارجی ندارد، برنامه نباید کامپایل شود. یکبار این را بررسی کنید. برنامه بدون مشکل کامپایل می‌شود! اما تنها در زمان اجرا است که صفحه زرد رنگ معروف خطاهای ASP.NET ظاهر می‌شود که چنین خاصیتی وجود ندارد (این حالت پیش فرض است؛ یعنی کامپایل یک View‌ در زمان اجرا). البته این باز هم خیلی بهتر است از ViewBag، چون اگر مثلا ViewBag.Country1 را وارد کنیم، در زمان اجرا تنها چیزی نمایش داده نخواهد شد؛‌ اما با روش Strongly typed view، حتما خطای Compilation Error به همراه نمایش محل مشکل نهایی، در صفحه ظاهر خواهد شد.
سؤال: آیا می‌شود پیش از اجرای برنامه هم این بررسی را انجام داد؟
پاسخ: بله. باید فایل پروژه را اندکی ویرایش کرده و مقدار MvcBuildViews را که به صورت پیش فرض false هست، true نمود. یا خارج از ویژوال استودیو با یک ادیتور متنی ساده مثلا فایل csproj را گشوده و این تغییر را انجام دهید. یا داخل ویژوال استودیو، بر روی نام پروژه کلیک راست کرده و سپس گزینه Unload Project را انتخاب کنید. مجددا بر روی این پروژه Unload شده کلیک راست نموده و گزینه edit را انتخاب نمائید. در صفحه باز شده، MvcBuildViews را یافته و آن‌را true کنید. سپس پروژه را Reload کنید.
اکنون اگر پروژه را کامپایل کنید، پیغام خطای زیر پیش از اجرای برنامه قابل مشاهده خواهد بود:

'MvcApplication1.Models.Employee' does not contain a definition for 'FirstName1' 
and no extension method 'FirstName1' accepting a first argument of type 'MvcApplication1.Models.Employee'
could be found (are you missing a using directive or an assembly reference?)
d:\Prog\MvcApplication1\MvcApplication1\Views\Home\Index.cshtml 10 MvcApplication1

البته بدیهی است این تغییر، زمان Build پروژه را مقداری افزایش خواهد داد؛ اما امن‌ترین حالت ممکن برای جلوگیری از این نوع خطاهای تایپی است.
یا حداقل بهتر است یکبار پیش از ارائه نهایی برنامه این مورد فعال و بررسی شود.

و یک خبر خوب!
مجوز سورس کد ASP.NET MVC از MS-PL به Apache تغییر کرده و همچنین Razor و یک سری موارد دیگر هم سورس باز شده‌اند. این تغییرات به این معنا خواهند بود که پروژه از حالت فقط خواندنی MS-PL به حالت متداول یک پروژه سورس باز که شامل دریافت تغییرات و وصله‌ها از جامعه برنامه نویس‌ها است، تغییر کرده است (^ و ^).

اشتراک‌ها
دوره ASP.NET Core SignalR

Everything you need to get started adding real time functionality to your web apps using ASP.NET Core SignalR.  

دوره ASP.NET Core SignalR
مطالب
خلاصه اشتراک‌های روز چهار شنبه 1390/06/30

نظرات مطالب
مفاهیم برنامه نویسی ـ مروری بر کلاس و شیء
سلام و ممنون از نظر شما.
اتفاق جالبی افتاد و آن این بود که هم اکنون داشتم در OneNote بخشی برای مطلب بعدی می‌نوشتم. دقیقاً داشتم پاراگرافی را می‌نوشتم که جلوی این که ذهن خواننده به سمتی برود که گویی "الزاماً هر مورد در مهندسی نرم افزار را باید پس از یافتن مصداق آن در محیط اطراف یاد گرفت" را بگیرم.
دقیقاً صحیح است. تاکید بر این تناظر در این بخش به دلیل یافتن درک عمیق‌تر از شیء گرایی و علت مفید بودن آن و چگونگی شکل گیری ایده آن است. این درک عمیق‌تر امکان استفاده بهتر و صحیح‌تر این مفاهیم در برنامه را فراهم می‌کند. و سبب می‌شود برنامه نویس شیءگرایی را ابزاری برای حل مسئله بیاد نه راه و روشی که همه میگن خوبه پس باید رعایت کرد. حال آنکه چون درک دقیقی از آن ندارد در حقیقت مسئله را با آن روشی که بهتر بلد است حل می‌کنند و فقط تعدادی کلاس و شیء در برنامه وجود دارد.
نظرات مطالب
استفاده از MediaWiki بهترین روش نگهداری یادداشت‌های شخصی خصوصا برای برنامه‌نویس‌ها
من کلا از OneNote در این زمینه‌ها استفاده گسترده‌ای دارم. از چند بابت برام خیلی کاربردی بوده:
- پشتیبانی کامل فارسی و ویرایشگری بی‌نقص‌اش
- همیشه در دسترس است و نیاز به ذخیره‌سازی ندارد
- افزونه‌های کلیپ کردن مطلب هایت‌لایت شده و ارسال مستقیم اون از مرورگر به وان‌نت
- ایجاد صفحات همراه با زیرشاخه درست کردن به نحوی که از نظر منطقی و زیبایی عالی عمل میکنه.
و ممنون از شما بابت معرفی این روش. حتما امتحانش میکنم. امیدوارم از این دست مطالب پویا و سازنده بیشتر باشه. هرکس به‌قدر بضاعت خودش. مخصوصا در فضایی که اینجارو اینقدر پرشور و هیجان کرده.
مطالب
مخفی کردن کوئری استرینگ‌ها در ASP.NET MVC توسط امکانات Routing
ابتدا مدل و منبع داده نمونه زیر را در نظر بگیرید:
using System.Collections.Generic;

namespace TestRouting.Models
{
    public class Issue
    {
        public int IssueId { set; get; }
        public int ProjectId { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
    }

    public static class IssuesDataSource
    {
        public static IList<Issue> CreateDataSource()
        {
            var results = new List<Issue>();
            for (int i = 0; i < 100; i++)
            {
                results.Add(new Issue { IssueId = i, ProjectId = i, Body = "Test...", Title = "Title " + i });
            }
            return results;
        }
    }
}
به همراه کنترلر زیر برای نمایش لیست اطلاعات و همچنین نمایش جزئیات یک issue انتخابی:
using System.Linq;
using System.Web.Mvc;
using TestRouting.Models;

namespace TestRouting.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var issuesList = IssuesDataSource.CreateDataSource();
            return View(issuesList); //show the list
        }

        public ActionResult Details(int issueId, int projectId)
        {
            var issue = IssuesDataSource.CreateDataSource()
                                        .Where(x => x.IssueId == issueId && x.ProjectId == projectId)
                                        .FirstOrDefault();
            return View(issue);
        }
    }
}
و View زیر کار نمایش لیست بازخوردهای یک پروژه را انجام می‌دهد:
@model IEnumerable<TestRouting.Models.Issue>
@{
    ViewBag.Title = "Index";
}
<h2>
    Issues</h2>
<ul>
    @foreach (var item in Model)
    {
   
        <li>
            @Html.ActionLink(linkText: item.Title,
                     actionName: "Details",
                     controllerName: "Home",
                     routeValues: new { issueId = item.IssueId, projectId = item.ProjectId },
                     htmlAttributes: null)
        </li>
    }
</ul>
در این حالت اگر پروژه را اجرا کنیم، هر لینک نمایش داده شده، چنین فرمی را خواهد داشت:
 http://localhost:1036/Home/Details?issueId=0&projectId=0
سؤال: آیا می‌شود این لینک‌ها را کمی زیباتر و SEO Friendly‌تر کرد؟
برای مثال آن‌را به نحو زیر نمایش داد:
 http://localhost:1036/Home/Details/0/0
پاسخ: بلی. برای اینکار تنها کافی است از امکانات مسیریابی استفاده کنیم:
using System.Web.Mvc;
using System.Web.Routing;

namespace TestRouting
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "IssueDetails",
                url: "Details/{issueId}/{projectId}", //تطابق با یک چنین مسیرهایی
                defaults: new 
                          { 
                                controller = "Home", //کنترلری که این نوع مسیرها را پردازش خواهد کرد
                                action = "Details", // اکشن متدی که نهایتا پارامترها را دریافت می‌کند
                                issueId = UrlParameter.Optional, //این خواص نیاز است هم نام پارامترهای اکشن متد تعریف شوند
                                projectId = UrlParameter.Optional
                          }
            );

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}
در اینجا یک route جدید به نام دلخواه IssueDetails پیش از route پیش فرض، تعریف شده است.
این route جدید با مسیرهایی مطابق پارامتر url آن تطابق خواهد یافت. پس از آن کوئری استرینگ متناظر با issueId را به پارامتر issueId اکشن متدی به نام Details و کنترلر Home ارسال خواهد کرد؛ به همین ترتیب در مورد projectId عمل خواهد شد.
ضمنا در url نهایی نمایش داده شده، دیگر اثری از کوئری استرینگ‌ها نبوده و برای نمونه در این حالت، اولین لینک نمایش داده شده شکل زیر را خواهد داشت:
 http://localhost:1036/Home/Details/0/0
 البته باید دقت داشت، یک چنین اصلاح خودکاری تنها در حالت استفاده از متد Html.ActionLink رخ می‌دهد و اگر Urlها را دستی ایجاد کنید، تغییری را مشاهده نخواهید کرد.
پاسخ به بازخورد‌های پروژه‌ها
راهنمایی خروجی HTML
هدف اصلی این کتابخانه، تهیه خروجی چاپی دقیق هست و قابل انتقال به سیستم‌های دیگر. در این بین سه نوع خروجی XML، CSV و Excel پیش فرض هم وجود دارند که داخل فایل PDF پیوست می‌شوند. امکان نوشتن نوع‌های دیگر نیز وجود دارد (با پیاده سازی اینترفیس IDataExporter). برای ایده می‌توانید به سورس این خروجی‌های پیش فرض مراجعه کنید.