مطالب
تعیین تعداد ردیف در صفحه جداول خودکار iTextSharp

پیشنیاز : «تکرار خودکار سرستون‌های یک جدول در صفحات مختلف، توسط iTextSharp»
همانطور که در مطلب پیشنیاز عنوان شده ذکر گردید، iTextSharp امکان درج خودکار header و footer به علاوه محاسبه خودکار تعداد ردیف‌های یک جدول در یک صفحه را بر اساس طول و اندازه محتوای هر ردیف، دارد. برای مثال یک صفحه ممکن است 2 ردیف شود و یک صفحه 20 ردیف. تمام این‌ها را به صورت خودکار محاسبه می‌کند و بسیار عالی است. (این امکان مهمی است که خیلی از ابزارهای گزارشگیری موجود هنوز با آن مشکل دارند)
اما اگر فرض را بر این بگذاریم که اندازه سلول‌ها و در نتیجه طول هر ردیف ثابت است و مثلا تمام صفحات نهایتا از یک تعداد ردیف مشخص تشکیل خواهند شد، خاصیتی را به نام number of rows یا rows count و امثال آن‌را ندارد که مثلا به آن گفت، من در هر صفحه فقط 5 ردیف را می‌خواهم نمایش دهم و نه 20 ردیف را.
روش حل این مساله را در ادامه ملاحظه خواهید کرد و یک نکته‌ی خیلی ساده و مستند نشده دارد!

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace RowsCountSample
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var table1 = new PdfPTable(3);
table1.HeaderRows = 2;
table1.FooterRows = 1;

//header row
var headerCell = new PdfPCell(new Phrase("header"));
headerCell.Colspan = 3;
headerCell.HorizontalAlignment = Element.ALIGN_CENTER;
table1.AddCell(headerCell);

//footer row
var footerCell = new PdfPCell(new Phrase("footer"));
footerCell.Colspan = 3;
footerCell.HorizontalAlignment = Element.ALIGN_CENTER;
table1.AddCell(footerCell);

//adding some rows
for (int i = 0; i < 70; i++)
{
//adds a new row
table1.AddCell(new Phrase("Cell[0], Row[" + i + "]"));
table1.AddCell(new Phrase("Cell[1], Row[" + i + "]"));
table1.AddCell(new Phrase("Cell[2], Row[" + i + "]"));

//sets the number of rows per page
if (i > 0 && table1.Rows.Count % 7 == 0)
{
pdfDoc.Add(table1);
table1.DeleteBodyRows();
pdfDoc.NewPage();
}
}

pdfDoc.Add(table1);
}

//open the final file with adobe reader for instance.
Process.Start("Test.pdf");
}
}
}

نکته جدید این مثال، قسمت زیر است:


if (i > 0 && table1.Rows.Count % 7 == 0)
{
pdfDoc.Add(table1);
table1.DeleteBodyRows();
pdfDoc.NewPage();
}

هر زمان که table1 به صفحه اضافه شود، header و footer هم اضافه خواهند شد، اما اگر BodyRows آن حذف نشود،‌ دفعه‌ی دومی که این table به صفحه اضافه می‌شود، شامل ردیف‌های مثلا یک تا 10 خواهد بود بجای 6 تا 10 .

مطالب
پیاده سازی Full-Text Search با SQLite و EF Core - قسمت اول - ایجاد و به روز رسانی جدول مجازی FTS
SQLite به صورت توکار از full-text search پشتیبانی می‌کند؛ اما اهمیت آن چیست؟ هدف از full-text search، انجام جستجوهای بسیار سریع، در ستون‌های متنی یک جدول بانک اطلاعاتی است. بدون وجود یک چنین قابلیتی، عموما برای انجام اینکار از دستور LIKE استفاده می‌شود:
SELECT Title FROM Book WHERE Desc LIKE '%cat%';
کار این کوئری، یافتن ردیف‌هایی است که در آن واژه‌ی cat وجود دارند. مشکل این روش، عدم استفاده‌ی از ایندکس‌ها و اصطلاحا انجام یک full table scan است. با استفاده از دستور LIKE، باید تک تک ردیف‌های بانک اطلاعاتی برای یافتن واژه‌ی مدنظر، اسکن و بررسی شوند و انجام اینکار با بالا رفتن تعداد رکوردهای بانک اطلاعاتی، کندتر و کندتر خواهد شد. برای رفع این مشکل، راه حلی به نام full-text search ارائه شده‌است که کار آن ایندکس کردن تمام ستون‌های متنی مدنظر و سپس جستجوی بر روی این ایندکس از پیش آماده شده‌است.
معادل دستور LIKE در کوئری فوق، متد Contains در EF Core است:
var cats = context.Chapters.Where(item => item.Text.Contains("cat")).ToList();
بنابراین هدف از این سری، جایگزین کردن متدهای الحاقی Contains ، StartsWith و EndsWith، با روشی بسیار سریعتر است.

یک نکته: کوئری فوق توسط EF Core و به همراه پروایدر SQLite آن، به صورت زیر ترجمه می‌شود (که آن نیز یک full table scan است):
SELECT  "c"."Text" FROM "Chapters" AS "c" WHERE ('cat' = '') OR (instr("c"."Text", 'cat') > 0)
اما دقیقا دستور Like را به همراه متدهای الحاقی StartsWith و یا EndsWith می‌توان مشاهده کرد:
var cats = context.Chapters.Where(item => item.Text.StartsWith("cat")).ToList();
// SELECT "c"."Text", FROM "Chapters" AS "c" WHERE "c"."Text" IS NOT NULL AND ("c"."Text" LIKE 'cat%')
var cats = context.Chapters.Where(item => item.Text.EndsWith("cat")).ToList();
// SELECT "c"."Title" FROM "Chapters" AS "c" WHERE "c"."Text" IS NOT NULL AND ("c"."Text" LIKE '%cat')


معرفی موجودیت‌های مثال این سری

هدف اصلی ما، ایندکس کردن full-text ستون‌های متنی عنوان و متن جدول بانک اطلاعاتی متناظر با Chapter است:
using System.Collections.Generic;

namespace EFCoreSQLiteFTS.Entities
{
    public class User
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public ICollection<Chapter> Chapters { get; set; }
    }

    public class Chapter
    {
        public int Id { get; set; }

        public string Title { get; set; }

        public string Text { get; set; }

        public User User { get; set; }
        public int UserId { get; set; }
    }
}


ایجاد جدول مجازی Full-text search

زمانیکه عملیات Migration را در EF Core فعال و اجرا می‌کنیم، دو جدول متناظر با Chapter و User ایجاد می‌شوند. اما برای کار با full-text search، نیاز به ایجاد جداول دیگری است، تا کار نگهداری ایندکس‌های تشکیل شده‌ی از ستون‌های متنی مدنظر ما را انجام دهند. به این نوع جداول در SQLite، جدول مجازی و یا virtual table گفته می‌شود. یک virtual table در اصل تفاوتی با یک جدول معمولی ندارد. تفاوت در اینجا است که منطق دسترسی به این جدول مجازی از موتور FTS5 مربوط به SQLite باید عبور کند. تاکنون نگارش‌های مختلفی از موتور full-text search آن منتشر شده‌اند؛ مانند FTS3 ، FTS4 و غیره که آخرین نگارش آن، FTS5 می‌باشد و به همراه توزیعی که مایکروسافت ارائه می‌دهد، وجود دارد و نیازی به تنظیمات خاصی ندارد.
در اینجا روش ایجاد یک جدول مجازی جدید Chapters_FTS را مشاهده می‌کنید:
CREATE VIRTUAL TABLE "Chapters_FTS"
USING fts5("Text", "Title", content="Chapters", content_rowid="Id")
جدول مجازی، با اجرای دستور CREATE VIRTUAL TABLE  ایجاد می‌شود و USING fts5 آن به معنای استفاده‌ی از موتور full-text search نگارش پنجم آن است. سپس لیست ستون‌هایی را که می‌خواهیم ایندکس کنیم، ذکر می‌شوند؛ مانند Text و Title در اینجا. همانطور که مشاهده می‌کنید، فقط نام این ستون‌ها قابل تعریف هستند و هیچ نوع اطلاعات اضافه‌تری را نمی‌توان ذکر کرد.
ذکر پارامتر "content="Chapters اختیاری بوده و به این معنا است که نیازی نیست تا اصل داده‌های مرتبط با ستون‌های ذکر شده نیز ذخیره شوند و آن‌ها را می‌توان از جدول Chapters، بازیابی کرد. در این حالت برای برقراری ارتباط بین این جدول مجازی و جدول chapters، پارامتر "content_rowid="Id مقدار دهی شده‌است. content_rowid به primary key جدول content اشاره می‌کند. ذکر هر دوی این پارامترها اختیاری بوده و در صورت تنظیم، حجم نهایی بانک اطلاعاتی را کاهش می‌دهند. چون در این حالت دیگری نیازی به ذخیره سازی جداگانه‌ی اصل اطلاعات متناظر با ایندکس‌های FTS نیست.

اکنون که با دستور ایجاد جدول مجازی FTS آشنا شدیم، روش ایجاد آن در برنامه‌های مبتنی بر EF Core نیز دقیقا به همین صورت است:
private static void createFtsTables(ApplicationDbContext context)
{
    // For SQLite FTS
    // Note: This can be added to the `protected override void Up(MigrationBuilder migrationBuilder)` method too.
    context.Database.ExecuteSqlRaw(@"CREATE VIRTUAL TABLE IF NOT EXISTS ""Chapters_FTS""
    USING fts5(""Text"", ""Title"", content=""Chapters"", content_rowid=""Id"");");
}
فقط کافی است در ابتدای اجرای برنامه با استفاده از متد ExecuteSqlRaw، عبارت SQL متناظر با ایجاد جدول مجازی را اجرا کنیم. این یک روش ایجاد این نوع جداول است؛ روش دیگر آن، قرار دادن همین قطعه کد در متد "protected override void Up(MigrationBuilder migrationBuilder)" مربوط به کلاس‌های ایجاد شده‌ی توسط عملیات Migration است.


به روز رسانی اطلاعات جدول مجازی FTS، توسط تریگرها

پس از اجرای دستورCREATE VIRTUAL TABLE  فوق، SQLite پنج جدول را به صورت خودکار ایجاد می‌کند که در تصویر زیر قابل مشاهده هستند:


البته ما مستقیما با این جداول کار نخواهیم کرد و این جداول برای نگهداری اطلاعات ایندکس‌های full-text موتور FTS5، توسط خود SQLite نگهداری و مدیریت می‌شوند.

اما ... نکته‌ی مهم اینجا است که جدول مجازی Chapters_FTS، هرچند به جدول اصلی Chapters توسط پارامتر content آن متصل شده‌است، اما تغییرات آن‌را ردیابی نمی‌کند. یعنی هر نوع insert/update/delete ای که در جدول اصلی Chapters رخ می‌دهد، سبب ایندکس شدن اطلاعات جدید آن در جدول مجازی Chapters_FTS نمی‌شود و برای اینکار باید اطلاعات را مستقیما در جدول Chapters_FTS درج کرد.
روش پیشنهاد شده‌ی در مستندات رسمی آن، استفاده از تریگرهای پس از درج اطلاعات، پس از حذف اطلاعات و پس از به روز رسانی اطلاعات به صورت زیر است:
-- Create a table. And an external content fts5 table to index it.
CREATE TABLE tbl(a INTEGER PRIMARY KEY, b, c);
CREATE VIRTUAL TABLE fts_idx USING fts5(b, c, content='tbl', content_rowid='a');

-- Triggers to keep the FTS index up to date.
CREATE TRIGGER tbl_ai AFTER INSERT ON tbl BEGIN
  INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;
CREATE TRIGGER tbl_ad AFTER DELETE ON tbl BEGIN
  INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
END;
CREATE TRIGGER tbl_au AFTER UPDATE ON tbl BEGIN
  INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
  INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;
در اینجا ابتدا روش ایجاد یک جدول جدید و سپس ایجاد یک جدول مجازی FTS را از روی آن مشاهده می‌کنید.
در ادامه سه تریگر بر روی جدول اصلی که ما به صورت متداولی با آن در برنامه‌های خود کار می‌کنیم، تعریف شده‌اند. این تریگرها کار insert اطلاعات را در جدول مجازی ایجاد شده، به صورت خودکار انجام می‌دهند.
همانطور که مشاهده می‌کنید، یک rowid نیز در اینجا قابل تعریف است؛ rowid، ستون مخفی یک جدول مجازی FTS است و هرچند در حین ایجاد، آن‌را ذکر نمی‌کنیم، اما جزئی از ساختار آن بوده و قابل کوئری گرفتن است.

نکته‌ی مهم: به فرمت دستورات به روز رسانی جدول مجازی FTS دقت کنید. حتی در حالت تریگرهای update و یا delete نیز در اینجا دستور insert، مشاهده می‌شوند. این فرمت دقیقا باید به همین نحو رعایت شود؛ در غیراینصورت اگر از دستورات delete و یا update معمولی بر روی این جدول مجازی استفاده کنید، دفعه‌ی بعدی که برنامه را اجرا می‌کنید، خطای «این بانک اطلاعاتی تخریب شده‌است» را مشاهده کرده (database disk image is malformed) و دیگر نمی‌توانید با فایل بانک اطلاعاتی خود کار کنید.


به روز رسانی اطلاعات جدول مجازی FTS توسط EF Core

روش تعریف تریگرهای یاد شده، مستقل از EF Core بوده و راسا توسط خود بانک اطلاعاتی مدیریت می‌شود. بنابراین فقط کافی است دستور CREATE TRIGGER را به همان نحوی که عنوان شد، توسط متد ExecuteSqlRaw اجرا کنیم تا جزئی از ساختار بانک اطلاعاتی شوند؛ اما ... این روش برای برنامه‌هایی با متن‌های پیچیده کارآیی ندارد. برای مثال فرض کنید اطلاعات اصلی شما با فرمت HTML است. ایندکس ایجاد شده، تگ‌های HTML را حذف نمی‌کند و آن‌ها را نیز ایندکس می‌کند که نه تنها سبب بالا رفتن حجم بانک اطلاعاتی می‌شود، بلکه زمانیکه ما قصد جستجویی را بر روی اطلاعات HTML ای داریم، اساسا کاری به تگ‌های آن نداشته و هدف اصلی ما، متن‌های درج شده‌ی در آن است. نمونه‌ی دیگر آن داشتن اطلاعاتی با «اعراب» است و یا شاید نیاز به یک‌دست سازی ی و ک فارسی وجود داشته باشد. به این نوع عملیات، «نرمال سازی متن» گفته می‌شود و با روش تریگرهای فوق قابل تعریف و مدیریت نیست. به همین جهت می‌توان از روش پیشنهادی زیر استفاده کرد:

الف) یافتن لیست اطلاعات تغییر یافته‌ی حاصل از اعمال insert/update/delete
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace EFCoreSQLiteFTS.DataLayer
{
    public static class EFChangeTrackerExtensions
    {
        public static List<(EntityState State, TEntity NewEntity, TEntity OldEntity)>
                    GetChangedEntities<TEntity>(this DbContext dbContext) where TEntity : class, new()
        {
            if (!dbContext.ChangeTracker.AutoDetectChangesEnabled)
            {
                // ChangeTracker.Entries() only calls `Try`DetectChanges() behind the scene.
                dbContext.ChangeTracker.DetectChanges();
            }

            return dbContext.ChangeTracker.Entries<TEntity>()
                    .Where(IsEntityChanged)
                    .Select(entityEntry => (entityEntry.State,
                                            entityEntry.Entity,
                                            createWithValues<TEntity>(entityEntry.OriginalValues)))
                    .ToList();
        }

        private static bool IsEntityChanged(EntityEntry entry)
        {
            return entry.State == EntityState.Added
                    || entry.State == EntityState.Modified
                    || entry.State == EntityState.Deleted
                    || entry.References.Any(r => r.TargetEntry?.Metadata.IsOwned() == true && IsEntityChanged(r.TargetEntry));
        }

        private static T createWithValues<T>(PropertyValues values) where T : new()
        {
            var entity = new T();
            foreach (var prop in values.Properties)
            {
                var value = values[prop.Name];
                if (value is PropertyValues)
                {
                    throw new NotSupportedException("nested complex object");
                }
                else
                {
                    prop.PropertyInfo.SetValue(entity, value);
                }
            }
            return entity;
        }
    }
}
هدف از متد GetChangedEntities فوق این است که با استفاده از سیستم tracking، نوع عملیات انجام شده و همچنین اصل موجودیت‌ها را پیش و پس از تغییر، بتوان لیست کرد و سپس بر اساس آن‌ها، جدول مجازی FTS را به روز رسانی نمود.
علت نیاز به نمونه‌ی اصل و سپس تغییر کرده‌ی موجودیت‌ها، به نحوه‌ی تعریف تریگرهای مخصوص به به روز رسانی FTS بر می‌گردد. اگر دقت کرده باشید در این تریگرها، new.a و همچنین old.a را داریم که برای شبیه سازی آن‌ها دقیقا باید به اطلاعات یک رکورد، در پیش و پس از به روز رسانی آن، دسترسی یافت.

ب) تعریف تریگرهای SQL توسط سیستم tracking؛ به همراه عملیات نرمال سازی اطلاعات
using System.Collections.Generic;
using System.Data;
using System.Text.RegularExpressions;
using EFCoreSQLiteFTS.Entities;
using Microsoft.EntityFrameworkCore;

namespace EFCoreSQLiteFTS.DataLayer
{
    public static class FtsNormalizer
    {
        private static readonly Regex _htmlRegex = new Regex("<[^>]*>", RegexOptions.Compiled);

        public static string NormalizeText(this string text)
        {
            if (string.IsNullOrWhiteSpace(text))
            {
                return string.Empty;
            }

            // Remove html tags
            text = _htmlRegex.Replace(text, string.Empty);

            // TODO: add other normalizers here, such as `remove diacritics`, `fix Persian Ye-Ke` and so on ...

            return text;
        }
    }

    public static class UpdateFtsTriggers
    {
        public static void UpdateChapterFTS(
            this DbContext context,
            List<(EntityState State, Chapter NewEntity, Chapter OldEntity)> changedChapters)
        {
            var database = context.Database;

            try
            {
                database.BeginTransaction(IsolationLevel.ReadCommitted);

                foreach (var (State, NewEntity, OldEntity) in changedChapters)
                {
                    var chapterNew = NewEntity;
                    var chapterOld = OldEntity;

                    var normalizedNewText = chapterNew.Text.NormalizeText();
                    var normalizedOldText = chapterOld.Text.NormalizeText();
                    var normalizedNewTitle = chapterNew.Title.NormalizeText();
                    var normalizedOldTitle = chapterOld.Title.NormalizeText();
                    switch (State)
                    {
                        case EntityState.Added:
                            if (shouldSkipAddedChapter(chapterNew))
                            {
                                continue;
                            }
                            database.ExecuteSqlRaw("INSERT INTO Chapters_FTS(rowid, Text, Title) values({0}, {1}, {2});",
                                    chapterNew.Id, normalizedNewText, normalizedNewTitle);
                            break;
                        case EntityState.Modified:
                            if (shouldSkipModifiedChapter(chapterNew, chapterOld))
                            {
                                continue;
                            }
                            // This format is important! Otherwise we will get `SQLite Error 11: 'database disk image is malformed'.` error!
                            database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS(Chapters_FTS, rowid, Text, Title)
                                                        VALUES('delete', {0}, {1}, {2}); ",
                                                        chapterOld.Id, normalizedOldText, normalizedOldTitle);
                            database.ExecuteSqlRaw("INSERT INTO Chapters_FTS(rowid, Text, Title) values({0}, {1}, {2});",
                                    chapterNew.Id, normalizedNewText, normalizedNewTitle);
                            break;
                        case EntityState.Deleted:
                            // This format is important! Otherwise we will get `SQLite Error 11: 'database disk image is malformed'.` error!
                            database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS(Chapters_FTS, rowid, Text, Title)
                                                        VALUES('delete', {0}, {1}, {2}); ",
                                    chapterOld.Id, normalizedOldText, normalizedOldTitle);
                            break;
                    }
                }
            }
            finally
            {
                database.CommitTransaction();
            }
        }

        private static bool shouldSkipAddedChapter(Chapter chapterNew)
        {
            // TODO: add your logic to avoid indexing this item
            return false;
        }

        private static bool shouldSkipModifiedChapter(Chapter chapterNew, Chapter chapterOld)
        {
            // TODO: add your logic to avoid indexing this item
            return chapterNew.Text == chapterOld.Text && chapterNew.Title == chapterOld.Title;
        }
    }
}
در اینجا نحوه‌ی تعریف متد UpdateChapterFTS را مشاهده می‌کند که اطلاعات خودش را از متد GetChangedEntities دریافت کرده و سپس یکی یکی آن‌ها را در جدول مجازی FTS، با فرمت مخصوصی که عنوان شد (دقیقا متناظر با فرمت تریگرهای مستندات رسمی FTS)، درج می‌کند.
همچنین در اینجا متد NormalizeText را نیز مشاهده می‌کند که بر روی ستون‌های متنی اعمال شده‌است. کار آن پاکسازی تگ‌های یک متن HTML ای است و نگهداری اطلاعات صرفا متنی آن. در اینجا اگر نیاز بود می‌توان منطق‌های پاکسازی اطلاعات دیگری را نیز اعمال کرد.
اکنون که این اطلاعات به صورت پاکسازی شده در جدول مجازی درج می‌شوند، زمانیکه بر روی آن‌ها جستجویی صورت می‌گیرد، دیگر شامل جستجوی بر روی تگ‌های HTML ای نیست و دقت بسیار بیشتری دارد.

ج) اتصال به سیستم
پس از تعریف متدهای الحاقی GetChangedEntities و UpdateChapterFTS، اکنون روش اتصال آن‌ها به DbContext برنامه، با بازنویسی متد SaveChanges آن است:
namespace EFCoreSQLiteFTS.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Chapter> Chapters { get; set; }
        public DbSet<User> Users { get; set; }

        public override int SaveChanges()
        {
            var changedChapters = this.GetChangedEntities<Chapter>();

            this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChanges();
            this.ChangeTracker.AutoDetectChangesEnabled = true;

            this.UpdateChapterFTS(changedChapters);
            return result;
        }
    }
}
از این پس تمام عملیات insert/update/delete برنامه تحت کنترل قرار گرفته و به صورت خودکار سبب به روز رسانی جدول مجازی FTS نیز می‌شوند.


در قسمت بعدی، روش کوئری گرفتن از این جدول مجازی FTS را بررسی می‌کنیم.
مطالب
یافتن Contextهای Dispose نشده در Entity framework
این دو متد را در نظر بگیرید:
        private static void disposedContext()
        {
            using (var context = new MyContext())
            {
                Debug.WriteLine("Posts count: " + context.BlogPosts.Count());
            }
        }

        private static void nonDisposedContext()
        {
            var context = new MyContext();
            Debug.WriteLine("Posts count: " + context.BlogPosts.Count());
        }
در اولی با استفاده از using، شیء context به صورت خودکار dispose خواهد شد؛ اما در دومی از using استفاده نشده‌است.

سؤال: در یک برنامه‌ی بزرگ چطور می‌توان لیست Contextهای Dispose نشده را یافت؟

در EF 6 با تعریف یک IDbConnectionInterceptor سفارشی می‌توان به متدهای باز، بسته و dispose شدن یک Connection دسترسی یافت. اگر Context ایی dispose نشده باشد، اتصال آن نیز dispose نخواهد شد.
using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;

namespace EFNonDisposedContext.Core
{
    public class DatabaseInterceptor : IDbConnectionInterceptor
    {
        public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
        {
            Connections.AddOrUpdate(connection, ConnectionStatus.Closed);
        }

        public void Disposed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
        {
            Connections.AddOrUpdate(connection, ConnectionStatus.Disposed);
        }

        public void Opened(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
        {
            Connections.AddOrUpdate(connection, ConnectionStatus.Opened);
        }
      
        // the rest of the IDbConnectionInterceptor methods ...

    }
}
همانطور که ملاحظه می‌کنید، با پیاده سازی IDbConnectionInterceptor، به سه متد Closed، Opened و Disposed یک DbConnection می‌توان دسترسی یافت.

مشکل مهم! در زمان فراخوانی متد Disposed، دقیقا کدام DbConnection باز شده، رها شده‌است؟
پاسخ به این سؤال را در مطلب «ایجاد خواص الحاقی» می‌توانید مطالعه کنید. با استفاده از یک ConditionalWeakTable به هر کدام از اشیاء DbConnection یک Id را انتساب خواهیم داد و پس از آن به سادگی می‌توان وضعیت این Id را ردگیری کرد.
برای این منظور، لیستی از ConnectionInfo را تشکیل خواهیم داد:
    public enum ConnectionStatus
    {
        None,
        Opened,
        Closed,
        Disposed
    }

    public class ConnectionInfo
    {
        public string ConnectionId { set; get; }
        public string StackTrace { set; get; }
        public ConnectionStatus Status { set; get; }

        public override string ToString()
        {
            return string.Format("{0}:{1} [{2}]",ConnectionId, Status, StackTrace);
        }
    }
در اینجا ConnectionId را به کمک ConditionalWeakTable محاسبه می‌کنیم.
StackTrace توسط نکته‌ی مطلب «کدام سلسله متدها، متد جاری را فراخوانی کرده‌اند؟ » تهیه می‌شود.
Status نیز وضعیت جاری اتصال است که بر اساس متدهای فراخوانی شده در پیاده سازی IDbConnectionInterceptor مشخص می‌گردد.

در پایان کار برنامه فقط باید یک گزارش تهیه کنیم از لیست ConnectionInfoهایی که Status آن‌ها مساوی Disposed نیست. این موارد با توجه به مشخص بودن Stack trace هر کدام، دقیقا محل متدی را که در آن context مورد استفاده dispose نشده‌است، مشخص می‌کنند.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
EFNonDisposedContext.zip
 
مطالب
استفاده از LINQ جهت تهیه کدهایی کوتاه‌تر و خواناتر

با کمک امکانات ارائه شده توسط LINQ ، می‌توان بسیاری از اعمال برنامه نویسی را در حجمی کمتر، خواناتر و در نتیجه با قابلیت نگهداری بهتر، انجام داد که تعدادی از آن‌ها را در ادامه مرور خواهیم کرد.

الف) تهیه یک یک رشته، حاوی عناصر یک آرایه، جدا شده با کاما.

using System.Linq;

public class CLinq
{
public static string GetCommaSeparatedListNormal(string[] data)
{
string items = string.Empty;

foreach (var item in data)
{
items += item + ", ";
}

return items.Remove(items.Length - 2, 1).Trim();
}

public static string GetCommaSeparatedList(string[] data)
{
return data.Aggregate((s1, s2) => s1 + ", " + s2);
}
}
همانطور که ملاحظه می‌کنید در روش دوم با استفاده از LINQ Aggregate extension method ، کد جمع و جورتر و خواناتری نسبت به روش اول حاصل شده است.

ب) پیدا کردن تعداد عناصر یک آرایه حاوی مقداری مشخص
برای مثال آرایه زیر را در نظر بگیرید:

var names = new[] { "name1", "name2", "name3", "name4", "name5", "name6", "name7" };
قصد داریم تعداد عناصر حاوی name را مشخص سازیم.
در تابع GetCountNormal زیر، این کار به شکلی متداول انجام شده و در GetCount از LINQ Count extension method کمک گرفته شده است.

using System.Linq;

public class CLinq
{
public static int GetCountNormal()
{
var names = new[] { "name1", "name2", "name3", "name4", "name5", "name6", "name7" };
var count = 0;
foreach (var name in names)
{
if (name.Contains("name"))
count += 1;
}
return count;
}

public static int GetCount()
{
var names = new[] { "name1", "name2", "name3", "name4", "name5", "name6", "name7" };
return names.Count(name => name.Contains("name"));
}
}
به نظر شما کدام روش خواناتر بوده و نگهداری و یا تغییر آن در آینده ساده‌تر می‌باشد؟

ج) دریافت لیستی از عناصر شروع شده با یک عبارت
در اینجا نیز دو روش متداول و استفاده از LINQ بررسی شده است.

using System.Linq;
using System.Collections.Generic;

public class CLinq
{
public static List<string> GetListNormal()
{
List<string> sampleList = new List<string>() { "A1", "A2", "P1", "P10", "B1", "B@", "J30", "P12" };
List<string> result = new List<string>();
foreach (var item in sampleList)
{
if (item.StartsWith("P"))
result.Add(item);
}
return result;
}

public static List<string> GetList()
{
List<string> sampleList = new List<string>() { "A1", "A2", "P1", "P10", "B1", "B@", "J30", "P12" };
return sampleList.Where(x => x.StartsWith("P")).ToList();
}
}

و در حالت کلی، اکثر حلقه‌های foreach متداول را می‌توان با نمونه‌های خواناتر کوئری‌های LINQ معادل، جایگزین کرد.

Vote on iDevCenter
مطالب
SortedSet در دات نت 4

SortedSet قرار گرفته در فضای نام System.Collections.Generic دات نت 4، لیستی از اشیاء به صورت خودکار مرتب شده را ارائه می‌دهد. SortedSet نیز همانند HashSet از اعضای منحصربفردی تشکیل خواهد شد اما اینبار به شکلی مرتب شده. برای پیاده سازی آن از red-black tree data structure استفاده شده است که مهم‌ترین مزیت آن امکان افزودن و یا حذف اشیاء به آن بدون کاهش قابل توجه کارآیی برنامه است.

مثال اول:
using System;
using System.Collections.Generic;

namespace SortedSetTest
{
class Program
{
static void sample1()
{
var setRange = new SortedSet<int> { 2, 5, 6, 2, 1, 4, 8 };

foreach (var i in setRange)
{
Console.WriteLine(i);
}
}

static void Main()
{
sample1();
}
}
}
در این مثال با نحوه‌ی ایجاد این لیست جنریک خود مرتب شونده‌ی تکراری نپذیر (!) آشنا می‌شوید. اگر این مثال را اجرا نمائید، خروجی آن مرتب شده است و همچنین تنها شامل یک عدد 2 است (اعضای تکراری را حذف می‌کند).

مثال دوم:

using System;
using System.Collections.Generic;

namespace SortedSetTest
{
class Program
{
static void sample2()
{
var setRange = new SortedSet<int>();
var random = new Random();

for (int counter = 0; counter < 100; counter++)
{
var rnd = random.Next(-180, 181);
if (!setRange.Add(rnd))
{
Console.WriteLine("Couldn't add {0}", rnd);
}
}

Console.WriteLine("Result set:");
foreach (var item in setRange)
{
Console.WriteLine(item);
}
}

static void Main()
{
sample2();
}
}
}
در این مثال نحوه‌ی افزودن اعضای مختلف به این لیست ویژه، توسط متد Add آن بیان شده است. اگر آیتمی در این لیست موجود باشد، مجددا اضافه نشده و حاصل متد Add آن، False خواهد بود.

مثال سوم:
اگر از سایر انواع سفارشی تعریف شده استفاده نمائید، باید روش مقایسه‌ی آن‌ها را نیز با پیاده سازی اینترفیس استاندارد IComparable ارائه دهید؛ در غیر اینصورت با خطای At least one object must implement IComparable متوقف خواهید شد.

using System;
using System.Collections;
using System.Collections.Generic;

namespace SortedSetTest
{
class FileInfo
{
public string Name { set; get; }
public long Size { set; get; }
}

class FileInfoComparer : IComparer<FileInfo>
{
public int Compare(FileInfo x, FileInfo y)
{
var caseiComp = new CaseInsensitiveComparer();
return caseiComp.Compare(x.Name, y.Name);
}
}


class Program
{

static void sample3()
{
var setRange = new SortedSet<FileInfo>(new FileInfoComparer())
{
new FileInfo
{
Name = "file1.txt",
Size = 100
},
new FileInfo
{
Name = "file2.txt",
Size = 10
},
new FileInfo
{
Name = "file3.txt",
Size = 300
}
};

foreach (var item in setRange)
{
Console.WriteLine(item.Name);
}
}

static void Main()
{
sample3();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

در این مثال اشیایی از نوع کلاس FileInfo به لیست ویژه‌ی ما اضافه شده‌اند. برای اینکه امکان مقایسه‌ی آن‌ها فراهم باشد ، کلاس FileInfoComparer با پیاده سازی اینترفیس IComparer ، روش مقایسه دو شیء از این دست را ارائه می‌دهد.

نظرات مطالب
تعیین شماره نگارش IE مورد استفاده در Web Browser Control
یک نکته‌ی تکمیلی
تعیین شماره نگارش IE مورد استفاده‌ی توسط برنامه به صورت خودکار:
using System;
using System.Diagnostics;
using Microsoft.Win32;
using System.Windows.Forms;

namespace Core
{
    public static class UseLatestVersionOfIE
    {
        /// <summary>
        /// Use the latest version of IE in WebBrowser control
        /// </summary>
        public static void SetWebBrowserVersion()
        {
            RegistryKey regkey = null;
            try
            {
                regkey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION", writable: true);
                if (regkey == null)
                {
                    return;
                }

                var regVal = getInstalledIEVersion();
                var appName = string.Format("{0}.exe", Process.GetCurrentProcess().ProcessName);
                regkey.SetValue(appName, regVal, RegistryValueKind.DWord);
            }
            catch (Exception ex)
            {
              // todo: log ...
            }
            finally
            {
                if (regkey != null)
                {
                    regkey.Close();
                }
            }
        }

        private static int getInstalledIEVersion()
        {
            int browserVer;
            using (var wb = new WebBrowser())
            {
                browserVer = wb.Version.Major;
            }

            int regVal;
            if (browserVer >= 11)
                regVal = 11001;
            else
                switch (browserVer)
                {
                    case 10:
                        regVal = 10001;
                        break;
                    case 9:
                        regVal = 9999;
                        break;
                    case 8:
                        regVal = 8888;
                        break;
                    default:
                        regVal = 7000;
                        break;
                }
            return regVal;
        }
    }
}
در اینجا شماره نگارش IE از کنترل WebBrowser دریافت می‌شود و همیشه به آخرین نگارش تنظیم خواهد شد و همچنین چون از Registry.CurrentUser استفاده می‌کند، نیازی به دسترسی مدیریتی برای اعمال ندارد.
مطالب
استفاده از Sparse Columns در SQL Server 2012
مقدمه
مقدار null به معنی پوچ و هیچ می‌باشد اما زمانی که در مقدار دهی جداول از آن استفاده می‌نمایم با توجه به نوع آن ستون فضای متفاوتی اشتغال می‌نماید. شاید در پایگاه داده‌های کوچک زیاد مطرح نباشد اما زمانی که حداقل چند گیگ حجم آن باشد و فرضا 20 تا 30 درصد آن از مقادیر null پر شده باشد فضای زیای از پوچ گرفته شده است این در حالی است که خیلی از توسعه دهندگان اصلا به اهمیت استفاده از null توجهی نمی‌کنند و از مقادیری غیر معتبری مثل 0 یا 1- در آن ستون به جای null استفاده می‌کنند.
SQL Server Sparce Columns
sparse column یا ستون‌های تنک قابلیتی از که از SQL Server 2008 اضافه شده و به ستون‌های عادی امکان استفاده بهینه از فضای ذخیره شده برای مقادیر null را می‌دهد. در واقع sparse column فضای مورد نیاز برای مقادیر null نسبت به مقادیر غیر null را کاهش می‌دهد. با استفاده از sparse column فضای ذخیره شده حداقل 20 تا 40 درصد کمتر خواهد شد.

ویژگی‌های Sparse Columns
  • SQL Server Database Engine از کلمه کلیدی SPARSE برای تعریف یک ستون که مقادیر آن می‌بایست بهینه شود استفاده می‌نماید.
  • نمای Catalog  جداول با ستون sparse شبیه جداول معمولی می‌باشد.
  • مقدار برگشتی از تابع COLUMNS_UPDATED با ستون sparce متفاوت از ستون معمولی است.
در نوع داده‌های زیر امکان استفاده از sparce columns  را ندارند:
 geography  text
 geometry   timestamp 
 image   user-defined data types 
ntext  
sparse column فضای بیشتری برای ذخیره داده‌های غیر null نسبت به داده‌های نشانه گذاری نشده با SPARSE لازم دارد و این فضا4 بایت بیشتر از ستون معمولی است. برآورد فضای ذخیره شده براساس نوع داده با طول ثابت در جدول زیر آورده شده است:
 نوع داده  بایت بدون sparse  بایت sparse  درصد null
 bit   0.125   5  98%
 tinyint   1   5  86%
 smallint   2  6  76%
 int   4  8  64%
bigint  8  12  52%
 real   4  8  64%
 float   8  12  52%
 smallmoney   4  8  64%
 money   8  12  52%
 smalldatetime   4  8  64%
 datetime   8  12  52%
 uniqueidentifier   16  20  43%
 date   3  7  69%
نوع داده با دقت - وابسته به طول
 
 نوع داده  بایت بدون sparse  یابت sparse  درصد null 
 (datetime(2   6  10  57%
 (datetime(2   8  12  52%
 (time(0   3  7  69%
 (time(7   5  9  60%
 (datetimetoffset(0   8  12  52%
 (datetimetoffset (7   10  14  49%
 (decimal/numeric(1,s   5  9  60%
 (decimal/numeric(38,s   17  21  42%
 (vardecimal(p,s      
نوع داده - داده وابسته به طول
نوع داده 
بایت بدون sparse   یابت sparse  درصد null
 sql_variant   2*   2*   60% 
 varchar or char   2*  4*+   60% 
 nvarchar or nchar   2*   4*   60% 
 varbinary or binary   2*   4*   60% 
 xml   2*   4*   60% 
 hierarchyid   2*  4*   60% 

محدویت‌های استفاده از Sparse columns
  • sparse column می‌ بایست nullable باشد و نمی‌تواند ROWGUIDCOL یا IDENTITY باشد. 
  • sparse column مقدار پیش فرض نمی‌تواند داشته باشد 
  • ستون محاسبه ای نمی‌تواند sparse باشد
  • sparse column نمی‌تواند بخشی از clustered index یا  unique primary key index باشد
  • sparse column  نمی تواند بخشی از  user-defined table باشد

مثالی از کاربرد Sparse columns
CREATE TABLE Employees_sparse (
   EMP_ID INT IDENTITY(5001,1) PRIMARY KEY, 
   SSN CHAR(9) NOT NULL, 
   TITLE CHAR(10) SPARSE NULL, 
   FIRSTNAME VARCHAR(50) NOT NULL, 
   MIDDLEINIT CHAR(1) SPARSE NULL, 
   LASTNAME VARCHAR(50) NOT NULL, 
   EMAIL CHAR(50) SPARSE NULL)
GO
CREATE TABLE Employees (
   EMP_ID INT IDENTITY(5001,1) PRIMARY KEY, 
   SSN CHAR(9) NOT NULL, 
   TITLE CHAR(10) NULL, 
   FIRSTNAME VARCHAR(50) NOT NULL, 
   MIDDLEINIT CHAR(1) NULL, 
   LASTNAME VARCHAR(50) NOT NULL, 
   EMAIL CHAR(50) NULL)
GO
در این دو جدول یکی با سه ستون Sparse  و دیگری بدون Sparse ایجاد شده و با  50000 ردیف داده پر شده است حال با رویه ذخیره شده sp_spaceused می‌توان فضای ذخیره شده دو جدول را باهم مقایسه نمود.
sp_spaceused 'Employees'
GO
sp_spaceused 'Employees_sparse' 

البته ذکر این نکته گفتی است که بهتر است از این تکنیک برای جداولی که تعداد زیادی ستون null دارند استفاده شود. 
مطالب
نحوه‌ی نگاشت فیلدهای فرمول در Fluent NHibernate

اگر با SQL Server کار کرده باشید حتما با مفهوم و امکان Computed columns (فیلدهای محاسبه شده) آن آشنایی دارید. چقدر خوب می‌شد اگر این امکان برای سایر بانک‌های اطلاعاتی که از تعریف فیلدهای محاسبه شده پشتیبانی نمی‌کنند، نیز مهیا می‌شد. زیرا یکی از اهداف مهم استفاده‌ی صحیح از ORMs ، مستقل شدن برنامه از نوع بانک اطلاعاتی است. برای مثال امروز می‌خواهیم با MySQL‌ کار کنیم، ماه بعد شاید بخواهیم یک نسخه‌ی سبک‌تر مخصوص کار با SQLite را ارائه دهیم. آیا باید قسمت دسترسی به داده برنامه را از نو بازنویسی کرد؟ اینکار در NHibernate فقط با تغییر نحوه‌ی اتصال به بانک اطلاعاتی میسر است و نه بازنویسی کل برنامه (و صد البته شرط مهم و اصلی آن هم این است که از امکانات ذاتی خود NHibernate استفاده کرده باشید. برای مثال وسوسه‌ی استفاده از رویه‌های ذخیره شده را فراموش کرده و به عبارتی ORM مورد استفاده را به امکانات ویژه‌ی یک بانک اطلاعاتی گره نزده باشید).
خوشبختانه در NHibernate امکان تعریف فیلدهای محاسباتی با کمک تعریف نگاشت خواص به صورت فرمول مهیا است. برای توضیحات بیشتر لطفا به مثال ذیل دقت بفرمائید:
در ابتدا کلاس کاربر تعریف می‌شود:

using System;
using NHibernate.Validator.Constraints;

namespace FormulaTests.Domain
{
public class User
{
public virtual int Id { get; set; }

[NotNull]
public virtual DateTime JoinDate { set; get; }

[NotNullNotEmpty]
[Length(450)]
public virtual string FirstName { get; set; }

[NotNullNotEmpty]
[Length(450)]
public virtual string LastName { get; set; }

[Length(900)]
public virtual string FullName { get; private set; } //از طریق تعریف فرمول مقدار دهی می‌گردد

public virtual int DayOfWeek { get; private set; }//از طریق تعریف فرمول مقدار دهی می‌گردد
}
}
در این کلاس دو خاصیت FullName و DayOfWeek به صورت فقط خواندنی به کمک private set ذکر شده، تعریف گردیده‌اند. قصد داریم روی این دو خاصیت فرمول تعریف کنیم:

using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;

namespace FormulaTests.Domain
{
public class UserCustomMappings : IAutoMappingOverride<User>
{
public void Override(AutoMapping<User> mapping)
{
mapping.Id(u => u.Id).GeneratedBy.Identity(); //ضروری است
mapping.Map(x => x.DayOfWeek).Formula("DATEPART(dw, JoinDate) - 1");
mapping.Map(x => x.FullName).Formula("FirstName + ' ' + LastName");
}
}
}
نحوه‌ی انتساب فرمول‌های مبتنی بر SQL را در نگاشت فوق ملاحظه می‌نمائید. برای مثال FullName از جمع دو فیلد نام و نام خانوادگی حاصل خواهد شد و DayOfWeek از طریق فرمول SQL دیگری که ملاحظه می‌نمائید (یا هر فرمول SQL دلخواه دیگری که صلاح می‌دانید).
اکنون اگر Fluent NHibernate را وادار به تولید اسکریپت متناظر با این دو کلاس کنیم حاصل به صورت زیر خواهد بود:
    create table Users (
UserId INT IDENTITY NOT NULL,
JoinDate DATETIME not null,
FirstName NVARCHAR(450) not null,
LastName NVARCHAR(450) not null,
primary key (UserId)
)
همانطور که ملاحظه می‌کنید در اینجا خبری از دو فیلد محاسباتی تعریف شده نیست. این فیلدها در تعاریف نگاشت‌ها به صورت خودکار ظاهر می‌شوند:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
default-access="property" auto-import="true" default-cascade="none" default-lazy="true">
<class xmlns="urn:nhibernate-mapping-2.2" mutable="true"
name="FormulaTests.Domain.User, FormulaTests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" table="Users">
<id name="Id" type="System.Int32" unsaved-value="0">
<column name="UserId" />
<generator class="identity" />
</id>
<property name="DayOfWeek" formula="DATEPART(dw, JoinDate) - 1" type="System.Int32" />
<property name="FullName" formula="FirstName + ' ' + LastName" type="System.String" />
<property name="JoinDate" type="System.DateTime">
<column name="JoinDate" />
</property>
<property name="FirstName" type="System.String">
<column name="FirstName" />
</property>
<property name="LastName" type="System.String">
<column name="LastName" />
</property>
</class>
</hibernate-mapping>
اکنون اگر کوئری زیر را در برنامه اجرا نمائیم:
var list = session.Query<User>.ToList();
foreach (var item in list)
{
Console.WriteLine("{0}:{1}", item.FullName, item.DayOfWeek);
}
به صورت خودکار به SQL ذیل ترجمه خواهد شد و اکنون نحوه‌ی بکارگیری فیلدهای فرمول، بهتر مشخص می‌گردد:
select
user0_.UserId as UserId0_,
user0_.JoinDate as JoinDate0_,
user0_.FirstName as FirstName0_,
user0_.LastName as LastName0_,
DATEPART(user0_.dw, user0_.JoinDate) - 1 as formula0_, --- همان فرمول تعریف شده است
user0_.FirstName + ' ' + user0_.LastName as formula1_ ---از طریق فرمول تعریف شده حاصل گردیده است
from
Users user0_

مطالب
تزریق وابستگی (dependency injection) به زبان ساده

این مطلب در ادامه‌ی "آشنایی با الگوی IOC یا Inversion of Control (واگذاری مسئولیت)" می‌باشد که هر از چندگاهی یک قسمت جدید و یا کاملتر از آن ارائه خواهد شد.

==============
به صورت خلاصه ترزیق وابستگی و یا dependency injection ، الگویی است جهت تزریق وابستگی‌های خارجی یک کلاس به آن، بجای استفاده مستقیم از آن‌ها در درون کلاس.
برای مثال شخصی را در نظر بگیرید که قصد خرید دارد. این شخص می‌تواند به سادگی با کمک یک خودرو خود را به اولین محل خرید مورد نظر برساند. حال تصور کنید که 7 نفر عضو یک گروه، با هم قصد خرید دارند. خوشبختانه چون تمام خودروها یک اینترفیس مشخصی داشته و کار کردن با آن‌ها تقریبا شبیه به یکدیگر است، حتی اگر از یک ون هم جهت رسیدن به مقصد استفاده شود، امکان استفاده و راندن آن همانند سایر خودروها می‌باشد و این دقیقا همان مطلبی است که هدف غایی الگوی تزریق وابستگی‌ها است. بجای این‌که همیشه محدود به یک خودرو برای استفاده باشیم، بنابر شرایط، خودروی متناسبی را نیز می‌توان مورد استفاده قرار داد.
در دنیای نرم افزار، وابستگی کلاس Driver ، کلاس Car است. اگر موارد ذکر شده را بدون استفاده از تزریق وابستگی‌ها پیاده سازی کنیم به کلاس‌های زیر خواهیم رسید:

//Person.cs
namespace DependencyInjectionForDummies
{
class Person
{
public string Name { get; set; }
}
}

//Car.cs
using System;
using System.Collections.Generic;

namespace DependencyInjectionForDummies
{
class Car
{
List<Person> _passengers = new List<Person>();

public void AddPassenger(Person p)
{
_passengers.Add(p);
Console.WriteLine("{0} added!", p.Name);
}

public void Drive()
{
foreach (var passenger in _passengers)
Console.WriteLine("Driving {0} ...!", passenger.Name);
}
}
}

//Driver.cs
using System.Collections.Generic;

namespace DependencyInjectionForDummies
{
class Driver
{
private Car _myCar = new Car();

public void DriveToMarket(IList<Person> passengers)
{
foreach (var passenger in passengers)
_myCar.AddPassenger(passenger);

_myCar.Drive();
}
}
}

//Program.cs
using System.Collections.Generic;
using System;

namespace DependencyInjectionForDummies
{
class Program
{
static void Main(string[] args)
{
new Driver().DriveToMarket(
new List<Person>
{
new Person{ Name="Ali" },
new Person{ Name="Vahid" }
});

Console.WriteLine("Press a key ...");
Console.ReadKey();
}
}
}

توضیحات:
کلاس شخص (Person) جهت تعریف مسافرین، اضافه شده؛ سپس کلاس خودرو (Car) که اشخاص را می‌توان به آن اضافه کرده و سپس به مقصد رساند، تعریف گردیده است. همچنین کلاس راننده (Driver) که بر اساس لیست مسافرین، آن‌ها را به خودروی خاص ذکر شده هدایت کرده و سپس آن‌ها را با کمک کلاس خودرو به مقصد می‌رساند؛ نیز تعریف شده است. در پایان هم یک کلاینت ساده جهت استفاده از این کلاس‌ها ذکر شده است.
همانطور که ملاحظه می‌کنید کلاس راننده به کلاس خودرو گره خورده است و این راننده همیشه تنها از یک نوع خودروی مشخص می‌تواند استفاده کند و اگر روزی قرار شد از یک ون کمک گرفته شود، این کلاس باید بازنویسی شود.

خوب! اکنون اگر این کلاس‌ها را بر اساس الگوی تزریق وابستگی‌ها (روش تزریق در سازنده که در قسمت قبل بحث شد) بازنویسی کنیم به کلاس‌های زیر خواهیم رسید:

//ICar.cs
using System;

namespace DependencyInjectionForDummies
{
interface ICar
{
void AddPassenger(Person p);
void Drive();
}
}

//Car.cs
using System;
using System.Collections.Generic;

namespace DependencyInjectionForDummies
{
class Car : ICar
{
//همانند قسمت قبل
}
}

//Van.cs
using System;
using System.Collections.Generic;

namespace DependencyInjectionForDummies
{
class Van : ICar
{
List<Person> _passengers = new List<Person>();

public void AddPassenger(Person p)
{
_passengers.Add(p);
Console.WriteLine("{0} added!", p.Name);
}

public void Drive()
{
foreach (var passenger in _passengers)
Console.WriteLine("Driving {0} ...!", passenger.Name);
}
}
}

//Driver.cs
using System.Collections.Generic;

namespace DependencyInjectionForDummies
{
class Driver
{
private ICar _myCar;

public Driver(ICar myCar)
{
_myCar = myCar;
}

public void DriveToMarket(IList<Person> passengers)
{
foreach (var passenger in passengers)
_myCar.AddPassenger(passenger);

_myCar.Drive();
}
}
}

//Program.cs
using System.Collections.Generic;
using System;

namespace DependencyInjectionForDummies
{
class Program
{
static void Main(string[] args)
{
Driver driver = new Driver(new Van());
driver.DriveToMarket(
new List<Person>
{
new Person{ Name="Ali" },
new Person{ Name="Vahid" }
});

Console.WriteLine("Press a key ...");
Console.ReadKey();
}
}
}

توضیحات:
در اینجا یک اینترفیس جدید به نام ICar اضافه شده است و بر اساس آن می‌توان خودروهای مختلفی را با نحوه‌ی بکارگیری یکسان اما با جزئیات پیاده سازی متفاوت تعریف کرد. برای مثال در ادامه، یک کلاس ون با پیاده سازی این اینترفیس تشکیل شده است. سپس کلاس راننده‌ی ما بر اساس ترزیق این اینترفیس در سازنده‌ی آن بازنویسی شده است. اکنون این کلاس دیگر نمی‌داند که دقیقا چه خودرویی را باید مورد استفاده قرار دهد و از وابستگی مستقیم به نوعی خاص از آن‌ها رها شده است؛ اما می‌داند که تمام خودروها، اینترفیس مشخص و یکسانی دارند. به تمام آن‌ها می‌توان مسافرانی را افزود و سپس به مقصد رساند. در پایان نیز یک راننده جدید بر اساس خودروی ون تعریف شده، سپس یک سری مسافر نیز تعریف گردیده و نهایتا متد DriveToMarket فراخوانی شده است.
به این صورت به یک سری کلاس اصطلاحا loosely coupled رسیده‌ایم. دیگر راننده‌ی ما وابسته‌ی به یک خودروی خاص نیست و هر زمانی که لازم بود می‌توان خودروی مورد استفاده‌ی او را تغییر داد بدون اینکه کلاس راننده را بازنویسی کنیم.
یکی دیگر از مزایای تزریق وابستگی‌ها ساده سازی unit testing کلاس‌های برنامه توسط mocking frameworks است. به این صورت توسط این نوع فریم‌ورک‌ها می‌توان رفتار یک خودرو را تقلید کرد بجای اینکه واقعا با تمام ریز جرئیات آن‌ها بخواهیم سروکار داشته باشیم (وابستگی‌ها را به صورت مستقل می‌توان آزمایش کرد).