مطالب
ذخیره و بازیابی فایل در Mongodb (بخش سوم)
در قسمت‌های پیشین (^ ،^ ) در مورد عملیات CRUD در سطح دیتابیس و به طور کلی در مورد ایندکس گذاری صحبت کردیم. در این بخش قصد داریم یکی از موارد بسیار مهم، یعنی ذخیره‌ی فایل‌های باینری را در دیتابیس، مورد بررسی قرار دهیم. روش‌های مختلفی برای اینکار وجود دارند؛ ولی بعضی از این روش‌ها در حال حاضر منسوخ شده اعلام شده‌اند که در اینجا ما آخرین روش را که در حال حاضر هیچ ویژگی منسوخ شده‌ای ندارد، به کار می‌گیریم.
از آنجاکه نهایت اندازه‌ی یک سند BSON نمی‌تواند بیشتر از 16 مگابایت باشد، قابلیتی به نام GridFS ایجاد شده‌است تا بتوان فایل‌های باینری را در آن ذخیره کرد. GridFS شامل دو بخش مختلف برای ذخیره اطلاعات یک فایل باینری است:
- fs.chunks که برای ذخیره اطلاعات قطعه‌های یک فایل باینری به کار میرود.
- fs.files که برای ذخیره اطلاعات و متادیتاها به کار می‌رود.

قبل از هر چیزی باید بدانید که کتابخانه مربوط به GridFs در یک پکیج جداگانه عرضه شده است و باید آن از طریق nuget نصب کنیم:
install-package MongoDB.Driver.GridFS

برای آپلود یک فایل باینری به داخل سیستم از کد زیر استفاده میکنیم:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
IGridFSBucket bucket = new GridFSBucket(db);
ابتدا یک شیء GridFsBucket را ایجاد میکنیم که از ما اطلاعات دیتابیس مورد نظر را برای ارسال فایل میخواهد و نتیجه‌ی آن یک کلاس از جنس اینترفیس IGridFSBucket می‌باشد. این باکت یا سطل در واقع همانند کالکشن رفتار میکند.
byte[] source=File.ReadAllBytes(@"D:\Untitled.png");
var options = new GridFSUploadOptions
            {
                ChunkSizeBytes = 64512, // 63KB
                Metadata = new BsonDocument
                {
                    { "CoverType", "Front" }, 
                    { "copyrighted", true }
                }
            };
در مرحله بعد فایل باینری را به صورت آرایه‌ای از بایت‌ها میخوانیم (البته حالت‌های مختلفی چون استریم را نیز پشتیبانی میکند). بعد از خواندن فایل، یک شیء از جنس کلاس GridFSUploadOptions را ایجاد و اطلاعات فایل آپلودی را مشخص می‌کنیم. به عنوان مثال اولین خصوصیتی که پر میکنیم خصوصیت تعیین سایز قطعات فایل باینری می‌باشد که به طور پیشفرض بر روی 64 مگابایت قرار گرفته است و عموما هم برای اکثر موارد، پاسخگوی نیاز‌ها است (در بخش بعدی مقالات، بیشتر این مورد را بررسی میکنیم).
مورد دومی که مقداردهی شده‌است، متادیتا‌ها هستند و این قابلیت را داریم که پرس و جوی خود را بر اساس آن‌ها نیز فیلتر کنیم. این خصوصیت مقدار دریافتی از جنس BsonDocument را دریافت میکند. ولی اگر شما برای فایل خود، کلاس اختصاصی برای متادیتاها در نظر گرفته‌اید میتوانید از یک Extension Method به نام ToBsonDocument استفاده کنید و شیء خود را به این نوع تبدیل کنید:
 var options = new GridFSUploadOptions
            {
                ChunkSizeBytes = 64512, // 63KB
                Metadata = metaData.ToBsonDocument()
            };


در نهایت آن را آپلود میکنیم:
 var id = bucket.UploadFromBytes("GoneWithTheWind", source, options);
پارامتر اول آن، نامی برای بسته آپلودی است. پارامتر دوم، خود فایل آپلودی میباشد و با پارمتر آخر هم تنظیماتی را که برای فایل مورد نظر توسط کلاس GridFSUploadOptions  تعیین کرده‌ایم، مشخص میکنیم. موقعیکه آپلود انجام شود، به ازای این کد، یک شناسه‌ی اختصاصی از جنس ObjectId را دریافت میکنیم که می‌توانیم آن را به یک خصوصیت در سند اصلی نسبت دهیم تا ارتباط بین سند و فایل هایش را داشته باشیم.

نکته: اگر در یک کلاس، چند فیلد از جنس ObjectId دارید، مونگو در بین تشخیص شناسه اصلی سند و شناسه تصاویر، با توجه به نام خصوصیت‌ها و غیره، تا حد زیادی هوشمند عمل میکند. ولی اگر خواستید صریحا شناسه اصلی را ذکر کنید و آن را متمایز از بقیه نشان دهید، می‌توانید از خصوصیت BsonId در بالای نام فیلد ID استفاده کنید:
[BsonId]
public ObjectId Id { get; set; }

جهت خواندن فایل آپلود شده، تنها کافی است از طریق شناسه‌ی دریافتی در مرحله‌ی آپلود، اقدام نماییم:
var bytes = bucket.DownloadAsBytes(id);

نکته: تمام متدهای آپلود و دانلود دیتا، هم به صورت آرایه ای از بایت‌ها و هم به صورت استریم میتوانند مورد استفاده قرار بگیرند و به ازای هر کدام، متدهای همزمان و غیرهمزمان نیز موجود هستند.


اگر قصد دارید بر اساس نام داده شده، فایل را دریافت کنید، ممکن است که چندین فایل، تحت یک نام ذخیره شده باشند که میتوانید در حالت‌های مختلفی این تصاویر را واکشی نمایید:
0 : فایل اصلی
1: اولین نسخه فایل
2: دومین نسخه فایل
و الی آخر...

1-: جدیدترین نسخه فایل (مقدار پیش فرض)
2-: نسخه ماقبل جدیدترین نسخه فایل
و الی آخر...


منظور از نسخه، فایل‌هایی با نامی موجود و از قبل ذخیره شده هستند که نسخه جدیدی از فایل قبلی بوده و فایل اول، فایل اصلی محسوب میشود.
برای درک بهتر مسئله، من تصاویر زیر را به ترتیب از سمت راست به سمت چپ، تحت یک نام، وارد سیستم میکنم:


اولین تصویر، تصویر اصلی محسوب می‌شود و بعد از آن، نسخه اول و نسخه دوم تصویر، وارد سیستم می‌شوند و تکه کد زیر از آنجاکه با مقدار پیش فرض پر شده‌است باید آخرین تصویر، تصویر سمت چپ را برای شما بر روی دیسک ذخیره کند:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
IGridFSBucket bucket = new GridFSBucket(db);
var image=bucket.DownloadAsBytesByName("City of Glass-cover");
File.WriteAllBytes(@"D:\a.jpg",image);

برای مقداردهی خواص بالا به شکل زیر عمل میکنیم:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
IGridFSBucket bucket = new GridFSBucket(db);
var options = new GridFSDownloadByNameOptions
            {
                Revision = 0
            };
var image=bucket.DownloadAsBytesByName("City of Glass-cover",options);
File.WriteAllBytes(@"D:\a.jpg",image);
با پاس کردن مقدار 0 به مونگو، اولین تصویر وارد شده، یعنی تصویر اصلی را دریافت می‌کنیم که اولین تصویر از سمت راست می‌شود. اگر مقادیر 1 یا 1- را پاس دهیم، چون تنها سه تصویر بیشتر نیست، در هر دو حالت تصویر دوم بازگردانده می‌شود.

برای بازگردانی تصاویر از طریق مقادیر موجود در متادیتا، باید از کلاس ویژه‌ای به نام GridFSFileInfo استفاده کنیم. در اینجا هم همانند روزهای اول، از کلاس بیلدر جهت ایجاد شرط استفاده میکنیم:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
IGridFSBucket bucket = new GridFSBucket(db);
var filter = Builders<GridFSFileInfo>.Filter.Gte(x => x.Length , 600);
var sort = Builders<GridFSFileInfo>.Sort.Descending(x => x.UploadDateTime);
var options =new GridFSFindOptions()
            {
                Limit = 3,
                Sort = sort
            };
var cursor = bucket.Find(filter, options);
var list = cursor.ToList();
در اینجا ابتدا مشخص کرده‌ایم که فایل مورد نظر باید حجمی بیشتر از 600 بایت داشته باشد و مرتب سازی آن به صورت نزولی، بر اساس زمان آپلود باشد. در عبارت لامبدا تعریف شده، می‌توانید خصوصیت‌های مختلف یک فایل از قبیل نام، حجم (سایز) ، زمان آپلود و ... را ببینید. سپس مرتب سازی و تعداد رکورد برگشتی از ابتدای جدول را مشخص میکنیم و از متد Find یا FindAsync جهت جست‌وجو استفاده میکنیم. با شکل گرفتن کوئری درخواست، لیستی از آن را تهیه میکنیم. مقدار بازگشتی این شیء، در واقع اسنادی از تصاویر هستند که میتوانید از طریق Id یا نام آن‌ها، فایل اصلی را واکشی نمایید.
برای یافتن تصاویر بر اساس متادیتاهای تعریف شده، از کد زیر استفاده میکنیم:
var filter = Builders<GridFSFileInfo>.Filter.Eq("metadata.CoverType","Front");

تغییر نام تصاویر
جهت ویرایش یک نام فایل از طریق متدهای زیر اقدام می‌نماییم:
bucket.Rename(id, newFilename);

//یا در حالت غیرهمزمان
await bucket.RenameAsync(id, newFilename);

حذف تصویر
bucket.Delete(id);

//یا
await bucket.DeleteAsync(id);

برای حذف کل bucket از طریق کد زیر اقدام می‌نماییم:
bucket.Drop();

//یا
await bucket.DropAsync();
 
در بخش بعدی Chunk را مورد بررسی قرار می‌دهیم.
مطالب
عبارات باقاعده‌ای در مورد کار با تگ‌ها

حذف تمامی تگ‌های یک عبارت HTML
این تابع و عبارت باقاعده به کار رفته در آن هنگام جستجو بر روی یک فایل html که حاوی انبوهی از تگ‌ها است می‌تواند مفید باشد و یا جهت حذف هر نوع فرمت اعمالی به یک متن.

private static readonly Regex _htmlRegex = new Regex("<.*?>", RegexOptions.Compiled);
/// <summary>
/// حذف تمامی تگ‌های موجود
/// </summary>
/// <param name="html">ورودی اچ تی ام ال</param>
/// <returns></returns>
public static string CleanTags(string html)
{
return _htmlRegex.Replace(html, string.Empty);
}

حذف یک تگ ویژه بدون حذف محتویات آن
فرض کنید می‌خواهید تمام تگ‌های script بکار رفته در یک محتوای html را حذف کنید.

private static readonly Regex _contentRegex = new Regex(@"<\/?script[^>]*?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);

/// <summary>
/// تنها حذف یک تگ ویژه
/// </summary>
/// <param name="html">ورودی اچ تی ام ال</param>
/// <returns></returns>
public static string CleanScriptTags(string html)
{
return _contentRegex.Replace(html, string.Empty);
}

حذف یک تگ خاص به همراه محتویات آن تگ
فرض کنید می‌خواهیم در محتوای html دریافتی اثری از تگ‌ها و کدهای جاوا اسکریپتی یافت نشود.

private static readonly Regex _safeStrRegex = new Regex(@"<script[^>]*?>[\s\S]*?<\/script>",
RegexOptions.Compiled | RegexOptions.IgnoreCase);

/// <summary>
/// حذف یک تگ ویژه به همراه محتویات آن
/// </summary>
/// <param name="html">ورودی اچ تی ام ال</param>
/// <returns></returns>
public static string CleanScriptsTagsAndContents(string html)
{
return _safeStrRegex.Replace(html, "");
}

و اگر فرض کنیم که متدهای فوق در کلاسی به نام CRegExHelper قرار گرفته‌اند، کلاس آزمون واحد آن به صورت زیر می‌تواند باشد:

using NUnit.Framework;

namespace testWinForms87
{
[TestFixture]
public class CTestRegExHelper
{
#region Methods (3)

// Public Methods (3)

[Test]
public void TestCleanScriptsTagsAndContents()
{
Assert.AreEqual(
CRegExHelper.CleanScriptsTagsAndContents("data1 <script> ... </script> data2"),
"data1 data2");
}

[Test]
public void TestCleanScriptTags()
{
Assert.AreEqual(
CRegExHelper.CleanScriptTags("<b>data1</b> <script> ... </script> data2"),
"<b>data1</b> ... data2");
}

[Test]
public void TestCleanTags()
{
Assert.AreEqual(
CRegExHelper.CleanTags("<b>data</b>"),
"data");
}

#endregion Methods
}

}




مطالب
فشرده سازی با فرمت 7z

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


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

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

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

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

}

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

نظرات مطالب
غیرمعتبر شدن کوکی‌های برنامه‌های ASP.NET Core هاست شده‌ی در IIS پس از ری‌استارت آن
یک نکته‌ی تکمیلی: روش محافظت از کلیدهای سیستم DataProtection با یک مجوز SSL

اگر برنامه‌های ASP.NET Core را اجرا کرده باشید، عموما در ابتدای آن یک پیام محافظت نشده بودن کلیدهای سیستم DataProtection را لاگ می‌کند. برای رفع این مشکل می‌توان این مراحل را طی کرد:
الف) نیاز به یک مجوز SSL داریم که دارای private key هم باشد.
برای این منظور سه دستور زیر را صادر کنید تا یک فایل pfx مناسب سیستم DataProtection تولید شود:
"C:\Program Files\Git\usr\bin\openssl.exe" genrsa -out private.key 2048
"C:\Program Files\Git\usr\bin\openssl.exe" req -new -x509 -key private.key -out publickey.cer -days 1398
"C:\Program Files\Git\usr\bin\openssl.exe" pkcs12 -export -out idp.pfx -inkey private.key -in publickey.cer
این دستورات از openssl.exe برنامه‌ی Git for windows استفاده می‌کنند. اگر فایل pfx نهایی دارای private key نباشد (روش فوق این مشکل را ندارد)، حین استفاده‌ی از آن در برنامه، با خطاهایی مانند «کلید یافت نشد» و یا «access denied» مواجه می‌شوید.

ب) خواندن فایل pfx در برنامه
روش خواندن فایل‌های pfx به صورت زیر است:
private static X509Certificate2 loadCertificateFromFile(string filePath, string password)
{
    // NOTE:
    // You should check out the identity of your application pool and make sure
    // that the `Load user profile` option is turned on, otherwise the crypto susbsystem won't work.

    // For decryption the certificate must be in the certificate store. It's a limitation of how EncryptedXml works.
    using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
    {
      store.Open(OpenFlags.ReadWrite);
      store.Add(new X509Certificate2(filePath, password, X509KeyStorageFlags.Exportable));
    }

    return new X509Certificate2(
            filePath,
            password,
            keyStorageFlags: X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet
                             | X509KeyStorageFlags.Exportable);
}
دو نکته در اینجا مهم هستند: اگر از IIS استفاده می‌کنید، روشن کردن گزینه‌ی «Load user profile» را در Application pool برنامه فراموش نکنید، تا سیستم RSA به خوبی کار کند. همچنین در اینجا قسمت store.Add الزامی است. از این جهت که ASP.NET Core برای کار decryption کلیدها، فقط به اطلاعات X509Store سیستم مراجعه می‌کند و کاری به فایل pfx ما ندارد.

ج) معرفی مجوز تولید شده به سیستم
دراینجا آخرین مرحله، ذکر متد ProtectKeysWithCertificate به همراه مجوزی است که تولید کردیم:
services
   .AddDataProtection()
   .SetDefaultKeyLifetime(...)
   .SetApplicationName(...)
   .ProtectKeysWithCertificate(loadCertificateFromFile("path ...", "123"));
اکنون اگر برنامه را اجرا کنید، از فایل pfx تولیدی، برای رمزنگاری کلیدهای سیستم DataProtection استفاده خواهد شد.
مطالب
ارسال ویدیو بصورت Async توسط Web Api
فریم ورک ASP.NET Web API صرفا برای ساخت سرویس‌های ساده‌ای که می‌شناسیم، نیست و در واقع مدل جدیدی برای برنامه نویسی HTTP است. کارهای بسیار زیادی را می‌توان توسط این فریم ورک انجام داد که در این مقاله به یکی از آنها می‌پردازم. فرض کنید می‌خواهیم یک فایل ویدیو را بصورت Asynchronous به کلاینت ارسال کنیم.

ابتدا پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب آن را MVC + Web API انتخاب کنید.


ابتدا به فایل WebApiConfig.cs در پوشه App_Start مراجعه کنید و مسیر پیش فرض را حذف کنید. برای مسیریابی سرویس‌ها از قابلیت جدید Attribute Routing استفاده خواهیم کرد. فایل مذکور باید مانند لیست زیر باشد.
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();
    }
}
حال در مسیر ریشه پروژه، پوشه جدیدی با نام Videos ایجاد کنید و یک فایل ویدیو نمونه بنام sample.mp4 در آن کپی کنید. دقت کنید که فرمت فایل ویدیو در مثال جاری mp4 در نظر گرفته شده اما به سادگی می‌توانید آن را تغییر دهید.
سپس در پوشه Models کلاس جدیدی بنام VideoStream ایجاد کنید. این کلاس مسئول نوشتن داده فایل‌های ویدیویی در OutputStream خواهد بود. کد کامل این کلاس را در لیست زیر مشاهده می‌کنید.
public class VideoStream
{
    private readonly string _filename;
    private long _contentLength;

    public long FileLength
    {
        get { return _contentLength; }
    }

    public VideoStream(string videoPath)
    {
        _filename = videoPath;
        using (var video = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            _contentLength = video.Length;
        }
    }

    public async void WriteToStream(Stream outputStream,
        HttpContent content, TransportContext context)
    {
        try
        {
            var buffer = new byte[65536];

            using (var video = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                var length = (int)video.Length;
                var bytesRead = 1;

                while (length > 0 && bytesRead > 0)
                {
                    bytesRead = video.Read(buffer, 0, Math.Min(length, buffer.Length));
                    await outputStream.WriteAsync(buffer, 0, bytesRead);
                    length -= bytesRead;
                }
            }
        }
        catch (HttpException)
        {
            return;
        }
        finally
        {
            outputStream.Close();
        }
    }
}

شرح کلاس VideoStream
این کلاس ابتدا دو فیلد خصوصی تعریف می‌کند. یکی filename_ که فقط-خواندنی است و نام فایل ویدیو درخواستی را نگهداری می‌کند. و دیگری contentLength_ که سایز فایل ویدیو درخواستی را نگهداری می‌کند.

یک خاصیت عمومی بنام FileLength نیز تعریف شده که مقدار خاصیت contentLength_ را بر می‌گرداند.

متد سازنده این کلاس پارامتری از نوع رشته بنام videoPath را می‌پذیرد که مسیر کامل فایل ویدیوی مورد نظر است. در این متد، متغیر‌های filename_ و contentLength_ مقدار دهی می‌شوند. نکته‌ی قابل توجه در این متد استفاده از پارامتر FileShare.Read است که باعث می‌شود فایل مورد نظر هنگام باز شدن قفل نشود و برای پروسه‌های دیگر قابل دسترسی باشد.

در آخر متد WriteToStream را داریم که مسئول نوشتن داده فایل‌ها به OutputStream است. اول از همه دقت کنید که این متد از کلمه کلیدی async استفاده می‌کند بنابراین بصورت asynchronous اجرا خواهد شد. در بدنه این متد متغیری بنام buffer داریم که یک آرایه بایت با سایز 64KB را تعریف می‌کند. به بیان دیگر اطلاعات فایل‌ها را در پکیج‌های 64 کیلوبایتی برای کلاینت ارسال خواهیم کرد. در ادامه فایل مورد نظر را باز می‌کنیم (مجددا با استفاده از FileShare.Read) و شروع به خواندن اطلاعات آن می‌کنیم. هر 64 کیلوبایت خوانده شده بصورت async در جریان خروجی نوشته می‌شود و تا هنگامی که به آخر فایل نرسیده ایم این روند ادامه پیدا می‌کند.
while (length > 0 && bytesRead > 0)
{
    bytesRead = video.Read(buffer, 0, Math.Min(length, buffer.Length));
    await outputStream.WriteAsync(buffer, 0, bytesRead);
    length -= bytesRead;
}
اگر دقت کنید تمام کد بدنه این متد در یک بلاک try/catch قرار گرفته است. در صورتی که با خطایی از نوع HttpException مواجه شویم (مثلا هنگام قطع شدن کاربر) عملیات متوقف می‌شود و در آخر نیز جریان خروجی (outputStream) بسته خواهد شد. نکته دیگری که باید بدان اشاره کرد این است که کاربر حتی پس از قطع شدن از سرور می‌تواند ویدیو را تا جایی که دریافت کرده مشاهده کند. مثلا ممکن است 10 پکیج از اطلاعات را دریافت کرده باشد و هنگام مشاهده پکیج دوم از سرور قطع شود. در این صورت امکان مشاهده ویدیو تا انتهای پکیج دهم وجود خواهد داشت.

حال که کلاس VideoStream را در اختیار داریم می‌توانیم پروژه را تکمیل کنیم. در پوشه کنترلر‌ها کلاسی بنام VideoControllerبسازید. کد کامل این کلاس را در لیست زیر مشاهده می‌کنید.
public class VideoController : ApiController
{
    [Route("api/video/{ext}/{fileName}")]
    public HttpResponseMessage Get(string ext, string fileName)
    {
        string videoPath = HostingEnvironment.MapPath(string.Format("~/Videos/{0}.{1}", fileName, ext));
        if (File.Exists(videoPath))
        {
            FileInfo fi = new FileInfo(videoPath);
            var video = new VideoStream(videoPath);

            var response = Request.CreateResponse();

            response.Content = new PushStreamContent((Action<Stream, HttpContent, TransportContext>)video.WriteToStream,
                new MediaTypeHeaderValue("video/" + ext));

            response.Content.Headers.Add("Content-Disposition", "attachment;filename=" + fi.Name.Replace(" ", ""));
            response.Content.Headers.Add("Content-Length", video.FileLength.ToString());

            return response;
        }
        else
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
    }
}

شرح کلاس VideoController
همانطور که می‌بینید مسیر دستیابی به این کنترلر با استفاده از قابلیت Attribute Routing تعریف شده است.

[Route("api/video/{ext}/{fileName}")]
نمونه ای از یک درخواست که به این مسیر نگاشت می‌شود:
api/video/mp4/sample
بنابراین این مسیر فرمت و نام فایل مورد نظر را بدین شکل می‌پذیرد. در نمونه جاری ما فایل sample.mp4 را درخواست کرده ایم.
متد Get این کنترلر دو پارامتر با نام‌های ext و fileName را می‌پذیرد که همان فرمت و نام فایل هستند. سپس با استفاده از کلاس HostingEnvironment سعی می‌کنیم مسیر کامل فایل درخواست شده را بدست آوریم.
string videoPath = HostingEnvironment.MapPath(string.Format("~/Videos/{0}.{1}", fileName, ext));
استفاده از این کلاس با Server.MapPath تفاوتی نمی‌کند. در واقع خود Server.MapPath نهایتا همین کلاس HostingEnvironment را فراخوانی می‌کند. اما در کنترلر‌های Web Api به کلاس Server دسترسی نداریم. همانطور که مشاهده می‌کنید فایل مورد نظر در پوشه Videos جستجو می‌شود، که در ریشه سایت هم قرار دارد. در ادامه اگر فایل درخواست شده وجود داشت وهله جدیدی از کلاس VideoStream می‌سازیم و مسیر کامل فایل را به آن پاس می‌دهیم.
var video = new VideoStream(videoPath);
سپس آبجکت پاسخ را وهله سازی می‌کنیم و با استفاده از کلاس PushStreamContent اطلاعات را به کلاینت می‌فرستیم.
var response = Request.CreateResponse();

response.Content = new PushStreamContent((Action<Stream, HttpContent, TransportContext>)video.WriteToStream, new MediaTypeHeaderValue("video/" + ext));

کلاس PushStreamContent در فضای نام System.Net.Http وجود دارد. همانطور که می‌بینید امضای Action پاس داده شده، با امضای متد WriteToStream در کلاس VideoStream مطابقت دارد.

در آخر دو Header به پاسخ ارسالی اضافه می‌کنیم تا نوع داده ارسالی و سایز آن را مشخص کنیم.
response.Content.Headers.Add("Content-Disposition", "attachment;filename=" + fileName);
response.Content.Headers.Add("Content-Length", video.FileLength.ToString());
افزودن این دو مقدار مهم است. در صورتی که این Header‌‌ها را تعریف نکنید سایز فایل دریافتی و مدت زمان آن نامعلوم خواهد بود که تجربه کاربری خوبی بدست نمی‌دهد. نهایتا هم آبجکت پاسخ را به کلاینت ارسال می‌کنیم. در صورتی هم که فایل مورد نظر در پوشه Videos پیدا نشود پاسخ NotFound را بر می‌گردانیم.
if(File.Exists(videoPath))
{
    // removed for bravity
}
else
{
    return Request.CreateResponse(HttpStatusCode.NotFound);
}
خوب، برای تست این مکانیزم نیاز به یک کنترلر MVC و یک View داریم. در پوشه کنترلر‌ها کلاسی بنام HomeController ایجاد کنید که با لیست زیر مطابقت داشته باشد.
public class HomeController : Controller
{
    // GET: Home
    public ActionResult Index()
    {
        return View();
    }
}
نمای این متد را بسازید (با کلیک راست روی متد Index و انتخاب گزینه Add View) و کد آن را مطابق لیست زیر تکمیل کنید.
<div>
    <div>
        <video width="480" height="270" controls="controls" preload="auto">
            <source src="/api/video/mp4/sample" type="video/mp4" />
            Your browser does not support the video tag.
        </video>
    </div>
</div>
همانطور که مشاهده می‌کنید یک المنت ویدیو تعریف کرده ایم که خواص طول، عرض و غیره آن نیز مقدار دهی شده اند. زیر تگ source متنی درج شده که در صورت لزوم به کاربر نشان داده می‌شود. گرچه اکثر مرورگرهای مدرن از المنت ویدیو پشتیبانی می‌کنند. تگ سورس فایلی با مشخصات sample.mp4 را درخواست می‌کند و نوع آن را نیز video/mp4 مشخص کرده ایم.

اگر پروژه را اجرا کنید می‌بینید که ویدیو مورد نظر آماده پخش است. برای اینکه ببینید چطور داده‌های ویدیو در قالب پکیج‌های 64 کیلو بایتی دریافت می‌شوند از ابزار مرورگرتان استفاده کنید. مثلا در گوگل کروم F12 را بزنید و به قسمت Network بروید. صفحه را یکبار مجددا بارگذاری کنید تا ارتباطات شبکه مانیتور شود. اگر به المنت sample دقت کنید می‌بینید که با شروع پخش ویدیو پکیج‌های اطلاعات یکی پس از دیگری دریافت می‌شوند و اطلاعات ریز آن را می‌توانید مشاهده کنید.

پروژه نمونه به این مقاله ضمیمه شده است. قابلیت Package Restore فعال شده و برای صرفه جویی در حجم فایل، تمام پکیج‌ها و محتویات پوشه bin حذف شده اند. برای تست بیشتر می‌توانید فایل sample.mp4 را با فایلی حجیم‌تر جایگزین کنید تا نحوه دریافت اطلاعات را با روشی که در بالا بدان اشاره شد مشاهده کنید.

AsyncVideoStreaming.rar  
پاسخ به بازخورد‌های پروژه‌ها
بازگرداندن Stream فایل از WCF
با تشکر از توجه شما
به نظرم در مثال سیلورلایت فایل PDF روی سرور ذخیره می‌شود و بعد به کاربر نمایش داده می‌شود
آیا لازم است که فایل روی سرور ذخیره شود یعنی آیا می‌توان فایل را به صورت Stream تولید کرد

.Generate(data => data.AsPdfStream(new MemoryStream()));

و بعد PdfStreamOutput آن را بازگرداند؟
نظرات مطالب
تبدیل html به pdf با کیفیت بالا
iTextSharp با Stream کار می‌کند. این استریم می‌تواند فایل استریم یا memory stream باشد؛ که من در اینجا از حالت memory stream استفاده می‌کنم.
ضمن اینکه اگر فایل استریم هم می‌بود چون نام فایل‌ها یکی نیست، تداخلی رخ نمی‌داد.
مطالب
ویژگی Static Using Statements در سی شارپ 6
مروری بر کاربردهای مختلف دستور Using تا پیش از ارائه‌ی سی شارپ 6
1- اضافه کردن فضاهای نام مختلف، برای سهولت دسترسی به اعضای آن:
using System.Collections.Generic;
2- تعریف نام مستعار (alias name) برای نوع داده‌ها و فضای نام‌ها
using BLL = DotNetTipsBLLLayer;//نام مستعار برای فضای نام
using EmployeeDomain = DotNetTipsBLLLayer.Employee;//نام مستعار برای یک نوع داده
3- تعریف یک بازه و مشخص کردن زمان تخریب یک شیء و آزاد سازی حافظه‌ی تخصیص داده شده:
using (var sqlConnection = new SqlConnection())
            {
                //کد 
            }
در سی شارپ 6 ، Static Using Statements برای بهبود کدنویسی و تمیز‌تر نوشتن کد‌ها ارائه شده‌است.
در ابتدا نحوه‌ی عملکرد اعضای static را مرور می‌کنیم. متغیر‌ها و متدهایی که با کلمه‌ی کلیدی static معرفی می‌شوند، اعلام می‌کنند که برای استفاده‌ی از آنها به نمونه سازی کلاس آن‌ها احتیاجی نیست و برای استفاده‌ی از آنها کافی است نام کلاس را تایپ کرده (بدون نوشتن new) و متد و یا خصوصیت مورد نظر را فراخوانی کنیم.
با معرفی ویژگی جدید Static Using Statement نوشتن نام کلاس برای فراخوانی اعضای استاتیک نیز حذف می‌شود.
اتفاق خوبی است اگر بتوان  اعضای استاتیک را همچون  Data Typeهای موجود در سی شارپ استفاده کرد. مثلا بتوان به جای ()Console.WrriteLine  نوشت ()WriteLine  
نحوه استفاده از این ویژگی: در ابتدای فایل و بخش معرفی کتابخانه‌ها بدین شکل عمل می‌کنیم using static namespace.className .
در بخش className،  نام کلاس استاتیک مورد نظر خود را می‌نویسیم .
مثال : 
 using static System.Console;
using static System.Math;

namespace dotnettipsUsingStatic
{
    class Program
    {
        static void Main(string[] args)
        {

            Write(" *** Cal Area *** ");
            int r = int.Parse(ReadLine());
            var result = Pow(r, 2) * PI;
            Write($"Area  is : {result}");
            ReadKey();
       }
    }
}

همان طور که در کدهای فوق می‌بینید، کلاس‌های Console و Math، در ابتدای فایل با استفاده از ویژگی جدید سی شارپ 6 معرفی شده‌اند و در بدنه برنامه تنها با فراخوانی نام متد‌ها و خصوصیت‌ها از آنها استفاده کرده ایم.
 
استفاده از ویژگی using static و Enum:
فرض کنید می‌خواهیم یک نوع داده‌ی شمارشی را برای نمایش جنسیت تعریف کنیم:
enum Gender
    {
        Male,
        Female
    }

تا قبل از سی شارپ 6 برای استفاده‌ی از نوع داده شمارشی بدین شکل عمل می‌کردیم: 

var gender = Gender.Male;

و اکنون بازنویسی استفاده‌ی ازEnum  به کمک ویژگی جدید static using statement :

در قسمت معرفی فضاهای نام بدین شکل عمل می‌کنیم: 

using static dotnettipsUsingStatic.Gender;

و در برنامه کافیست مستقیما نام اعضای Enum  را ذکر کنیم  .

var gender = Male;//تخصیص نوع داده شمارشی
WriteLine($"Employee Gender is : {Male}");//استفاده مستقیم از نوع داده شمارشی


استفاده از ویژگی using static و متد‌های الحاقی :

تا قبل از ارائه سی شارپ 6 اگر نیاز به استفاده‌ی از یک متد الحاقی خاص همچون where در فضای نام System.Linq.Enumeable داشتیم می‌بایستی فضای نام System.Linq را به طور کامل اضافه می‌کردیم و راهی برای اضافه کردن یک فضای نام خاص درون فضای نام بزرگتر وجود نداشت. 

اما با قابلیت جدید اضافه شده می‌توانیم بخشی از یک فضای نام  را اضافه کنیم:

  using static System.Linq.Enumerable;


متد‌های استاتیک و متد‌های الحاقی در زمان استفاده از ویژگی using static:

فرض کنید کلاس  static ای بنام MyStaticClass داشته باشیم که متد Print1  و  Print2 در آن تعریف شده باشند:

public static class MyStaticClass
    {
        public static void Print1(string parameter)
        {
            WriteLine(parameter);
        }
        public static void  Print2(this string parameter)
        {
            WriteLine(parameter);
        }

    }

برای استفاده از متد‌های تعریف شده به شکل زیر عمل می‌کنیم : 

//فراخوانی تابع استاتیک
Print1("Print 1");//روش اول
MyStaticClass.Print1("Prtint 1");//روش دوم
//فراخوانی متد الحاقی استاتیک
MyStaticClass.Print2("Print 2");
"print 2".Print2();


ویژگی‌های جدید ارائه شده در سی شارپ 6 برای افزایش خوانایی برنامه‌ها و تمیز‌تر شدن کد‌ها اضافه شده‌اند. در مورد ویژگی‌های ارائه شده در مقاله‌ی جاری این نکته مهم است که گاهی قید کردن نام کلاس‌ها خود سبب افزایش خوانایی کد‌ها می‌شود .

مطالب
استفاده از قابلیت Script Data اس کیوال سرور 2008 از طریق برنامه نویسی

همانطور که مطلع هستید قابلیت تهیه عبارات Insert از جداول یک دیتابیس، به صورت استاندارد به management studio 2008 اضافه شده است. برای استفاده از این قابلیت از طریق برنامه نویسی به صورت زیر می‌توان عمل نمود:

الف) سه ارجاع را به اسمبلی‌های زیر اضافه نمائید:
Microsoft.SqlServer.ConnectionInfo
Microsoft.SqlServer.Management.Sdk.Sfc
Microsoft.SqlServer.Smo

ب) اکنون کدی که عملیات Script Data را با استفاده از قابلیت‌های SMO ارائه می‌دهد به صورت زیر خواهد بود:

using System;
using System.Text;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Sdk.Sfc;

/// <summary>
/// تهیه اسکریپت رکوردها
/// </summary>
/// <param name="dbName">نام دیتابیس مورد نظر</param>
/// <param name="table">نام جدولی که باید اسکریپت شود</param>
/// <param name="instance">وهله سرور</param>
/// <param name="userName">نام کاربری جهت اتصال</param>
/// <param name="pass">کلمه عبور جهت اتصال به سرور</param>
/// <returns>اسکریپت تهیه شده</returns>
public static string ScriptData(string dbName, string table, string instance, string userName, string pass)
{
try
{
ServerConnection serverConnection = new ServerConnection(instance, userName, pass);
Server server = new Server(serverConnection);
Database database = server.Databases[dbName];
if (database == null) return string.Empty;
Table tb = database.Tables[table];
Scripter scripter = new Scripter(server) {Options = {ScriptData = true}};
if (tb == null) return string.Empty;
StringBuilder sb = new StringBuilder();
foreach (string s in scripter.EnumScript(new Urn[] {tb.Urn}))
sb.AppendLine(s);
return sb.ToString();
}
catch(Exception ex)
{
//todo: log ...
return string.Empty;
}
}

نکته:
اسمبلی‌های SMO به همراه مجموعه SQL Server 2008 نصب می‌شوند. بنابراین متد و برنامه‌ی فوق بر روی سروری با این مشخصات بدون مشکل اجرا خواهد شد. اما اگر قصد توزیع برنامه خود را دارید باید Microsoft SQL Server 2008 Management Objects را نیز به همراه برنامه خود نصب نمائید.

مطالب
ایجاد کوکی با jcookie
همانطور که از نامش پیداست jcookie یک پلاگین jquery است. این پلاگین به شما این اجازه را می‌دهد تا هر نوع داده ای را که مایل هستید از قبیل رشته‌ها، آرایه‌ها و object را در قالب json با رمزگذاری base 64 ذخیره نمایید. استفاده از این رمزگذاری باعث کوچکتر شدن حجم کوکی تا 70 درصد می‌شود. در این مقاله شما یاد می‌گیرید که چطور برای ذخیره و بازیابی کوکی از آن استفاده کرده و چگونه در یک زبان سمت سرور، مثل سی شارپ نیز کوکی مورد نظر را با همان فرمت بخوانید.
جهت دانلود فایل jcookie به  اینجا  مراجعه کنید.
ذخیره کوکی
برای ساخت یک کوکی به روش زیر اقدام می‌کنیم. استفاده از jCookies.$ دو خاصیت به نام‌های نام کوکی و مقدار کوکی را name & Value در دسترس ما می‌گذارد:
var d = new Date();         
             
                $.jCookies({
                    name: 'dotnettips.info',
                    value: { Title: 'ساخت کوکی با jcookie', Author: 'علی یگانه مقدم', Seen: d.getDate(), Favorite: true }
                });
همانطور که می‌بینید ذخیره اطلاعات توسط jcookie بسیار ساده و راحت بوده و هر نوع داده ای در آن به راحتی قابل ذخیره سازیست. برای مثال می‌توانید اطلاعات یک کلاس را خیلی راحت و سریع با آن ذخیره کنید. به طور پیش فرض تاریخ انقضای کوکی 27 روز بعد از ایجاد آن می‌باشد. در صورتی که تمایل دارید این تاریخ را تغییر دهید یکی از خاصیت‌های seconds,minutes,hours و days در دسترس شماست و مقادیری که جلوی آن‌ها به کارگرفته می‌شود باید نوع صحیح بوده و در صورتی که مقدار نامعتبر وارد شود خاصیت مورد نظر نادیده گرفته می‌شود.
$.jCookies ({ name : 'User', value : { username : 'Bob' , level : 5 }, minutes : 60 });
برای تغییر پیش فرض‌های ساخت کوکی مانند انقضای 27 روز به عدد پیش فرض خودتان فایل jcookies.js را باز کرده و تنظیمات پیش فرض آن را تغییر بدهید. برای تغییر دنبال کد زیر بگردید:
$.jCookies.defaults =
{
name : '',
value : '',
days : 27
}

بازیابی کوکی
برای بازیابی کوکی مجددا از jCookies.$ استفاده می‌شود ولی تنها باید یک خاصیت get که نام کوکی هست را بنویسید:
var values = $.jCookies ({ get: 'dotnettips.info' });
در صورتی که نام کوکی‌ای که درخواست کرده اید وجود نداشته باشد یا اینکه تاریخ انقضای آن سر رسیده است و از سیستم کلاینت حذف شده است یا اینکه هنگام درخواست کوکی با خطایی مواجه شده باشد، مقدا برگشتی false خواهد بود و اگر نیاز دارید که بدانید آیا نوع برگشتی false به خاطر خطا بوده است یا خیر یک خاصیت نوع بول به نام error هم اضافه می‌شود:
var values = $.jCookies({ get: 'Rutabaga', error: true });
در صورتی که خطایی داده شود response مقدار values در مرورگر کروم به شکل زیر خواهد بود. در هر مرورگر نحوه نمایش خطا می‌تواند متفاوت باشد.
Error : {
            arguments : undefined,
            message : "Invalid base64 data",
            stack : "—",
            type : undefined
        }
بازیابی همه کوکی ها
در صورتی که به خاصیت get مقدار * را بدهید تمامی کوکی‌ها برگشت داده خواهند شد و به صورت آرایه ای از نام کوکی‌ها در دسترسی خواهند بود:
 var values = $.jCookies({ get: '*' });
                alert(values["dotnettips.info"].Title);
                alert(values["data2"].Title);

حذف کوکی
نحوه کدنویسی حذف کوکی هم دقیقا مشابه خواندن کوکی است؛ با این تفاوت که به جای استفاده از خاصیت get از خاصیت erase استفاده می‌کنیم و با دادن نام کوکی به این خاصیت، کوکی حذف خواهد شد:
var value = $.jCookies({ erase: 'dotnettips.info' });
در صورتی که کوکی وجود داشته باشد، آن را حذف کرده و مقدار true را برگشت خواهد زد و در صورتی که کوکی وجود نداشته باشد مقدار false را بر میگرداند.

بازیابی کوکی در سمت سرور با سی شارپ
در این روش ما ابتدا با همان دستور معمولی دات نت یعنی page.request.cookie درخواست دریافت کوکی را می‌دهیم ولی از آنجا که در jcookie دو عمل روی داده‌ها صورت گرفته است باید دو کار اضافه‌تر را انجام داد:
  1. برگشت داده‌ها از حالت رمزگذاری base64
  2. داده‌ها در فرمت json هستند و باید به حالتی قابل استفاده در محیط شی گرا تبدیل شوند.
برای بازگردانی از حالت base64 از کلاس و متد Convert.FromBase64String در فضای نام system.convert استفاده می‌کنیم که آرایه ای از نوع بایت را بر میگرداند و از Encoding.UTF8.GetString هم برای decode کردن آرایه به نوع رشته استفاده می‌کنیم. تا به اینجای کار داده‌های ما به صورت یک json خوانا با فرمت string درآمده است. برای دسترسی به داده‌های موجود در این فرمت باید آن‌ها را Deserialize کنیم که این کار را از طریق کلاس JavaScriptSerializer  در فضای نام System.Web.Script.Serialization انجام می‌دهیم و از کلاس دیکشنری برای ذخیره داده‌های برگشتی استفاده می‌کنیم که نوع string برای نام خاصیت و نوع آبجکت برای ذخیره مقدار خاصیت خواهد بود.  یعنی برای بازگردانی اولین مثال بالا باید داده‌های در نوع دیکشنری به صورت زیر لیست شوند
 Title ایجاد کوکی با jcookie 
 Author  علی یگانه مقدم
 Seen  2015/1/14
 Favorite  true
byte[] from64 = Convert.FromBase64String(Page.Request.Cookies["dotnettips.info"].Value);
            string json = Encoding.UTF8.GetString(from64);
            Dictionary<string, object> article =new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(json);
            Page.Response.Write("Title: "+ (string)article["Title"]);
پشتیبانی از یونیکد
موقعی که من اولین مثال بالا را نوشتم و مقادیر را به صورت فارسی وارد کردم متوجه شدم که رشته‌های یونیکد را انکود نمی‌کند و در نتیجه زبان فارسی در آن پشتیبانی نمی‌شود. برای همین تغییراتی در فایل js ایجاد کرده و عبارت value قبل از تبدیل به base64 را به صورت utf-16 انکود کردم و در هنگام خواندن کوکی هم به صورت utf-16 دیکود کردم و مشکل زبان فارسی هم در این حالت حل شد. البته کدی که اضافه کردم قابلیت‌های انکودینگ بیشتری هم دارد.
فقط تنها مورد این هست که برای خواندن کوکی در سمت سرور باید یک تغییر کوچک یک کلمه ای بدهیم؛ باید کلمه UTF8 را به Unicode که می‌شود همان UTF-16 در کد تغییر دهیم، که به کد زیر تغییر خواهد یافت:
byte[] from64 = Convert.FromBase64String(Page.Request.Cookies["dotnettips.info"].Value);
            string json = Encoding.Unicode.GetString(from64);
            Dictionary<string, object> article =new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(json);
            Page.Response.Write("Title: "+ (string)article["Title"]);
برای دریافت jcookie با پشتیبانی از زبان فارسی به اینجا مراجعه کنید.
کدهای بالا در فایل زیر قرار گرفته اند.