مطالب
روشی برای مقایسه‌ی مقادیر تمام خواص دو شیء در آزمون‌های واحد
در زمان نوشتن تست‌های مختلف (Unit - Integration - UI) گاهی اوقات پیش می‌آید که بخواهید تمامی خصوصیت‌های یک شیء را تایید کنید. معمولا نوشتن اعتبارسنجی برای همه خصوصیت‌ها و همین طور پیام‌های استثناء برای هر یک در زمان عدم تایید اعتبار، کار بسیار زمانبری است. در این مقاله به شما نشان خواهم داد که چگونه با نوشتن یک اعتبارسنج عمومی از اتلاف زمان زیادی جلوگیری کنید.

با استفاده از کلاس زیر می‌توان کار اعتبارسنجی را با استفاده از Reflection به راحتی انجام داد. در اینجا برای اعتبارسنجی DateTime از کلاس DateTimeAssert استفاده کرده‌ایم.
public class PropertiesValidator<TK, T> where T : new() where TK : new()
{
    static TK _instance;

    public static TK Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new TK();
            }
            return _instance;
        }
    }

    public void Validate(T expectedObject, T realObject, params string[] propertiesNotToCompare)
    {
        var properties = realObject.GetType().GetProperties();
        foreach (var currentRealProperty in properties)
        {
            if (!propertiesNotToCompare.Contains(currentRealProperty.Name))
            {
                var currentExpectedProperty = expectedObject.GetType().GetProperty(currentRealProperty.Name);
                var exceptionMessage = $"The property {currentRealProperty.Name} of class {currentRealProperty.DeclaringType?.Name} was not as expected.";

                if (currentRealProperty.PropertyType != typeof(DateTime) && currentRealProperty.PropertyType != typeof(DateTime?))
                {
                    Assert.AreEqual( currentExpectedProperty.GetValue( expectedObject,
                                                                        null ),
                                        currentRealProperty.GetValue( realObject,
                                                                    null ),
                                        exceptionMessage );
                }
                else
                {
                    DateTimeAssert.Validate( currentExpectedProperty.GetValue( expectedObject,
                                                                                null ) as DateTime?,
                                                currentRealProperty.GetValue( realObject,
                                                                            null ) as DateTime?,
                                                TimeSpan.FromMinutes( 5 ) );
                }
            }
        }
    }
}


طرز استفاده

فرض کنید مدلی داریم با این مشخصات:
public class ObjectToAssert
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime LastVisit { get; set; }
}
و دو نمونه از آن را ایجاد کرده ایم:
var expectedObject = new ObjectToAssert
                        {
                            FirstName = "Vahid",
                            LastName = "Mohammad Taheri",
                            LastVisit = new DateTime( 2016, 11, 14, 0, 10, 50 )
                        };
var actualObject = new ObjectToAssert
                        {
                            FirstName = "Vahid",
                            LastName = "Mohammad Taheri",
                            LastVisit = new DateTime( 2016, 11, 14, 0, 13, 50 )
                        };
کلاسی را با ارث بری از PropertiesValidator ایجاد می‌کنیم:
public class ObjectToAssertValidator : PropertiesValidator<ObjectToAssertValidator, ObjectToAssert>
{
    public void Validate(ObjectToAssert expected, ObjectToAssert actual)
    {
        this.Validate(expected, actual, "FirstName");
    }
}

نکته
: در صورتی که می‌خواهید خصوصیتی را استثناء کنید از اعتبارسنجی، می‌توانید آن‌را به عنوان پارامتر سوم به بعد به تابع Validate ارسال کنید. طبق کد بالا FirstName به صورت استثناء تعریف شده است.


اکنون دو نمونه ساخته شده از ObjectToAssert بالا را با فراخوانی دستور زیر اعتبارسنجی می‌کنیم:
ObjectToAssertValidator.Instance.Validate(expectedObject, actualObject);
مطالب
EF Code First #14

ردیابی تغییرات در EF Code first

EF از DbContext برای ذخیره اطلاعات مرتبط با تغییرات موجودیت‌های تحت کنترل خود کمک می‌گیرد. این نوع اطلاعات توسط Change Tracker API جهت بررسی وضعیت فعلی یک شیء، مقادیر اصلی و مقادیر تغییر کرده آن در دسترس هستند. همچنین در اینجا امکان بارگذاری مجدد اطلاعات موجودیت‌ها از بانک اطلاعاتی جهت اطمینان از به روز بودن آن‌ها تدارک دیده شده است. ساده‌ترین روش دستیابی به این اطلاعات، استفاده از متد context.Entry می‌باشد که یک وهله از موجودیتی خاص را دریافت کرده و سپس به کمک خاصیت State خروجی آن، وضعیت‌هایی مانند Unchanged یا Modified را می‌توان به دست آورد. علاوه بر آن خروجی متد context.Entry، دارای خواصی مانند CurrentValues و OriginalValues نیز می‌باشد. OriginalValues شامل مقادیر خواص موجودیت درست در لحظه اولین بارگذاری در DbContext برنامه است. CurrentValues مقادیر جاری و تغییر یافته موجودیت را باز می‌گرداند. به علاوه این خروجی امکان فراخوانی متد GetDatabaseValues را جهت بدست آوردن مقادیر جدید ذخیره شده در بانک اطلاعاتی نیز ارائه می‌دهد. ممکن است در این بین، خارج از Context جاری، اطلاعات بانک اطلاعاتی توسط کاربر دیگری تغییر کرده باشد. به کمک GetDatabaseValues می‌توان به این نوع اطلاعات نیز دست یافت.
حداقل چهار کاربرد عملی جالب را از اطلاعات موجود در Change Tracker API می‌توان مثال زد که در ادامه به بررسی آن‌ها خواهیم پرداخت.


کلاس‌های مدل مثال جاری

در اینجا یک رابطه many-to-one بین جدول هزینه‌های اقلام خریداری شده یک شخص و جدول فروشندگان تعریف شده است:

using System;

namespace EF_Sample09.DomainClasses
{
public abstract class BaseEntity
{
public int Id { get; set; }

public DateTime CreatedOn { set; get; }
public string CreatedBy { set; get; }

public DateTime ModifiedOn { set; get; }
public string ModifiedBy { set; get; }
}
}

using System;

namespace EF_Sample09.DomainClasses
{
public class Bill : BaseEntity
{
public decimal Amount { set; get; }
public string Description { get; set; }

public virtual Payee Payee { get; set; }
}
}

using System.Collections.Generic;

namespace EF_Sample09.DomainClasses
{
public class Payee : BaseEntity
{
public string Name { get; set; }

public virtual ICollection<Bill> Bills { set; get; }
}
}


به علاوه همانطور که ملاحظه می‌کنید، این کلاس‌ها از یک abstract class به نام BaseEntity مشتق شده‌اند. هدف از این کلاس پایه تنها تامین یک سری خواص تکراری در کلاس‌های برنامه است و هدف از آن، مباحث ارث بری مانند TPH، TPT و TPC نیست.
به همین جهت برای اینکه این کلاس پایه تبدیل به یک جدول مجزا و یا سبب یکی شدن تمام کلاس‌ها در یک جدول نشود، تنها کافی است آن‌را به عنوان DbSet معرفی نکنیم و یا می‌توان از متد Ignore نیز استفاده کرد:

using System.Data.Entity;
using EF_Sample09.DomainClasses;

namespace EF_Sample09.DataLayer.Context
{
public class Sample09Context : MyDbContextBase
{
public DbSet<Bill> Bills { set; get; }
public DbSet<Payee> Payees { set; get; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Ignore<BaseEntity>();

base.OnModelCreating(modelBuilder);
}
}
}



الف) به روز رسانی اطلاعات Context در صورتیکه از متد context.Database.ExecuteSqlCommand مستقیما استفاده شود

در قسمت قبل با متد context.Database.ExecuteSqlCommand برای اجرای مستقیم عبارات SQL بر روی بانک اطلاعاتی آشنا شدیم. اگر این متد در نیمه کار یک Context فراخوانی شود، به معنای کنار گذاشتن Change Tracker API می‌باشد؛ زیرا اکنون در سمت بانک اطلاعاتی اتفاقاتی رخ داده‌اند که هنوز در Context جاری کلاینت منعکس نشده‌اند:

using System;
using System.Data.Entity;
using EF_Sample09.DataLayer.Context;
using EF_Sample09.DomainClasses;

namespace EF_Sample09
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample09Context, Configuration>());

using (var db = new Sample09Context())
{
var payee = new Payee { Name = "فروشگاه سر کوچه" };
var bill = new Bill { Amount = 4900, Description = "یک سطل ماست", Payee = payee };
db.Bills.Add(bill);

db.SaveChanges();
}

using (var db = new Sample09Context())
{
var bill1 = db.Bills.Find(1);
bill1.Description = "ماست";

db.Database.ExecuteSqlCommand("Update Bills set Description=N'سطل ماست' where id=1");
Console.WriteLine(bill1.Description);

db.Entry(bill1).Reload(); //Refreshing an Entity from the Database
Console.WriteLine(bill1.Description);

db.SaveChanges();
}
}
}
}

در این مثال ابتدا دو رکورد به بانک اطلاعاتی اضافه می‌شوند. سپس توسط متد db.Bills.Find، اولین رکورد جدول Bills بازگشت داده می‌شود. در ادامه، خاصیت توضیحات آن به روز شده و سپس با استفاده از متد db.Database.ExecuteSqlCommand نیز بار دیگر خاصیت توضیحات اولین رکورد به روز خواهد شد.
اکنون اگر مقدار bill1.Description را بررسی کنیم، هنوز دارای مقدار پیش از فراخوانی db.Database.ExecuteSqlCommand می‌باشد، زیرا تغییرات سمت بانک اطلاعاتی هنوز به Context مورد استفاده منعکس نشده است.
در اینجا برای هماهنگی کلاینت با بانک اطلاعاتی، کافی است متد Reload را بر روی موجودیت مورد نظر فراخوانی کنیم.



ب) یکسان سازی ی و ک اطلاعات رشته‌ای دریافتی پیش از ذخیره سازی در بانک اطلاعاتی

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

using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Reflection;
using EF_Sample09.DataLayer.Toolkit;
using EF_Sample09.DomainClasses;

namespace EF_Sample09.DataLayer.Context
{
public class MyDbContextBase : DbContext
{
public void RejectChanges()
{
foreach (var entry in this.ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Modified:
entry.State = EntityState.Unchanged;
break;

case EntityState.Added:
entry.State = EntityState.Detached;
break;
}
}
}

public override int SaveChanges()
{
applyCorrectYeKe();
auditFields();
return base.SaveChanges();
}

private void applyCorrectYeKe()
{
//پیدا کردن موجودیت‌های تغییر کرده
var changedEntities = this.ChangeTracker
.Entries()
.Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);

foreach (var item in changedEntities)
{
if (item.Entity == null) continue;

//یافتن خواص قابل تنظیم و رشته‌ای این موجودیت‌ها
var propertyInfos = item.Entity.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance
).Where(p => p.CanRead && p.CanWrite && p.PropertyType == typeof(string));

var pr = new PropertyReflector();

//اعمال یکپارچگی نهایی
foreach (var propertyInfo in propertyInfos)
{
var propName = propertyInfo.Name;
var val = pr.GetValue(item.Entity, propName);
if (val != null)
{
var newVal = val.ToString().Replace("ی", "ی").Replace("ک", "ک");
if (newVal == val.ToString()) continue;
pr.SetValue(item.Entity, propName, newVal);
}
}
}
}

private void auditFields()
{
// var auditUser = User.Identity.Name; // in web apps
var auditDate = DateTime.Now;
foreach (var entry in this.ChangeTracker.Entries<BaseEntity>())
{
// Note: You must add a reference to assembly : System.Data.Entity
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedOn = auditDate;
entry.Entity.ModifiedOn = auditDate;
entry.Entity.CreatedBy = "auditUser";
entry.Entity.ModifiedBy = "auditUser";
break;

case EntityState.Modified:
entry.Entity.ModifiedOn = auditDate;
entry.Entity.ModifiedBy = "auditUser";
break;
}
}
}
}
}


اگر به کلاس Context مثال جاری که در ابتدای بحث معرفی شد دقت کرده باشید به این نحو تعریف شده است (بجای DbContext از MyDbContextBase مشتق شده):
public class Sample09Context : MyDbContextBase
علت هم این است که یک سری کد تکراری را که می‌توان در تمام Contextها قرار داد، بهتر است در یک کلاس پایه تعریف کرده و سپس از آن ارث بری کرد.
تعاریف کامل کلاس MyDbContextBase را در کدهای فوق ملاحظه می‌کنید.
در اینجا کار با تحریف متد SaveChanges شروع می‌شود. سپس در متد applyCorrectYeKe کلیه موجودیت‌های تحت نظر ChangeTracker که تغییر کرده باشند یا به آن اضافه شده‌ باشند، یافت شده و سپس خواص رشته‌ای آن‌ها جهت یکسانی سازی ی و ک، بررسی می‌شوند.


ج) ساده‌تر سازی به روز رسانی فیلدهای بازبینی یک رکورد مانند DateCreated، DateLastUpdated و امثال آن بر اساس وضعیت جاری یک موجودیت

در کلاس MyDbContextBase فوق، کار متد auditFields، مقدار دهی خودکار خواص تکراری تاریخ ایجاد، تاریخ به روز رسانی، شخص ایجاد کننده و شخص تغییر دهنده یک رکورد است. به کمک ChangeTracker می‌توان به موجودیت‌هایی از نوع کلاس پایه BaseEntity دست یافت. در اینجا اگر entry.State آن‌ها مساوی EntityState.Added بود، هر چهار خاصیت یاد شده به روز می‌شوند. اگر حالت موجودیت جاری، EntityState.Modified بود، تنها خواص مرتبط با تغییرات رکورد به روز خواهند شد.
به این ترتیب دیگر نیازی نیست تا در حین ثبت یا ویرایش اطلاعات برنامه نگران این چهار خاصیت باشیم؛ زیرا به صورت خودکار مقدار دهی خواهند شد.


د) پیاده سازی قابلیت لغو تغییرات در برنامه

علاوه بر این‌ها در کلاس MyDbContextBase، متد RejectChanges نیز تعریف شده است تا بتوان در صورت نیاز، حالت موجودیت‌های تغییر کرده یا اضافه شده را به حالت پیش از عملیات، بازگرداند.



نظرات اشتراک‌ها
7 نکته کاربردی برای رعایت Clean code
سلام دوست عزیز 
یک سوال از این مقاله که گذاشتید ، خط زیر یعنی چی؟ چجوری کنترل خطا را آورده روی متد؟
 private static ArrayList readLines(String fileName) throws IOException { ... }
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 22 - توزیع برنامه توسط IIS
این 2 خط رو هم به BuildWebHost در Program.cs  اضافه کنید و سپس تست کنید:
  public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
             ...
                .CaptureStartupErrors(true)
                .UseSetting("detailedErrors", "true")
              ...
                .Build();

نظرات مطالب
ایجاد لیستی از کلاسی جنریک
ممنون از مطلب مفیدتون، به نظر شما این کد خواناتر نیست؟
 static T GetColumnData(IList list, string name)
        {
            return (T)list.Single(p => p.Name == name).Data
{
نظرات مطالب
نحوه اضافه کردن Auto-Complete به جستجوی لوسین در ASP.NET MVC و Web forms
- این هم یک روش دیگر هست. از آن روش‌های ذکر شده در پیشنیازها هم می‌توانید استفاده کنید.
- highlight کردن عبارت جستجو شده در عنوان بازگشت داده شده، توسط افزونه auto-complete انجام می‌شود؛ خودکار است.
بازخوردهای پروژه‌ها
مشکل در فوتر
با سلام و احترام
من از فوتر سفارشی استفاده کردم که مطلبی رو در هر صفحه نمایش داده شود
اما مشکل به این صورت است که در صفحه اول فوتر و جدول اصلی روی هم قرار میگیره اما در بقیه صفحات این مشکل وجود نداره آیا در استفاده من ایرادی وجود داره؟

        public void PageFinished(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData)
        {
            var pageSize = document.PageSize;
            var text = "صفحه " + writer.PageNumber + " از ";
            var textLen = _font.BaseFont.GetWidthPoint(text, _font.Size) + 17;
            var positionX = pageSize.Right - 10;
            var align = _direction == PdfRunDirection.RightToLeft ? Element.ALIGN_RIGHT : Element.ALIGN_LEFT;
            ColumnText.ShowTextAligned(
                canvas: _pdfContentByte,
                alignment: align,
                phrase: ReportMethod.SetFont(text, 20),
                x: positionX,
                y: pageSize.GetBottom(4),
                rotation: 0,
                runDirection: (int)_direction,
                arabicOptions: 0);
            var x = _direction == PdfRunDirection.RightToLeft ? positionX - textLen : positionX + textLen;
            _pdfContentByte.AddTemplate(_template, x, pageSize.GetBottom(4));
            //--------------------------------------
            if (_Info != null)
            {
                var table = new PdfGrid(1) { RunDirection = (int)_direction, WidthPercentage = 100 };

                string[] msgField = { "مدیر گروه", _Info.Where(sp => sp.ID == _MemberID).FirstOrDefault().InstKindName, _Info.Where(sp => sp.ID == 0).FirstOrDefault().InstKindName, "امور مالی", "معاون پشتیبانی" };
                string[] dataField = { "", _Info.Where(sp => sp.ID == _MemberID).FirstOrDefault().MasterName, _Info.Where(sp => sp.ID == 0).FirstOrDefault().MasterName, "", _Info.Where(sp => sp.ID == 1).FirstOrDefault().MasterName };
                var infoTable = new PdfGrid(msgField.Length) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, WidthPercentage = 100 };
                foreach (var item in msgField)
                {
                    infoTable.AddCell(ReportMethod.SetCell(item, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, true, 20));
                }
                foreach (var item in dataField)
                {
                    infoTable.AddCell(ReportMethod.SetCell(item, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, true, 20));
                }

                table.AddCell(infoTable);
                table.SetTotalWidth(new[] { pageSize.Width - document.LeftMargin - document.RightMargin });
                table.WriteSelectedRows(
                        rowStart: 0,
                        rowEnd: -1,
                        xPos: document.LeftMargin,
                        yPos: document.BottomMargin - 10,
                        canvas: writer.DirectContent);

            }
        }


مطالب دوره‌ها
ساخت یک Mini ORM با AutoMapper
Mini ORM‌ها برخلاف ORMهای کاملی مانند Entity framework یا NHibernate، کوئری‌های LINQ را تبدیل به SQL نمی‌کنند. در اینجا کار با SQL نویسی مستقیم شروع می‌شود و مهم‌ترین کار این کتابخانه‌ها، نگاشت نتیجه‌ی دریافتی از بانک اطلاعاتی به اشیاء دات نتی هستند. خوب ... AutoMapper هم دقیقا همین کار را انجام می‌دهد! بنابراین در ادامه قصد داریم یک Mini ORM را به کمک AutoMapper طراحی کنیم.


کلاس پایه AdoMapper

public abstract class AdoMapper<T> where T : class
{
    private readonly SqlConnection _connection;
 
    protected AdoMapper(string connectionString)
    {
        _connection = new SqlConnection(connectionString);
    }
 
    protected virtual IEnumerable<T> ExecuteCommand(SqlCommand command)
    {
        command.Connection = _connection;
        command.CommandType = CommandType.StoredProcedure;
        _connection.Open();
 
        try
        {
            var reader = command.ExecuteReader();
            try
            {
                return Mapper.Map<IDataReader, IEnumerable<T>>(reader);
            }
            finally
            {
                reader.Close();
            }
        }
        finally
        {
            _connection.Close();
        }
    }
 
    protected virtual T GetRecord(SqlCommand command)
    {
        command.Connection = _connection;
        _connection.Open();
        try
        {
            var reader = command.ExecuteReader();
            try
            {
                reader.Read();
                return Mapper.Map<IDataReader, T>(reader);
            }
            finally
            {
                reader.Close();
            }
        }
        finally
        {
            _connection.Close();
        }
    }
 
    protected virtual IEnumerable<T> GetRecords(SqlCommand command)
    {
        command.Connection = _connection;
        _connection.Open();
        try
        {
            var reader = command.ExecuteReader();
            try
            {
                return Mapper.Map<IDataReader, IEnumerable<T>>(reader);
            }
            finally
            {
                reader.Close();
            }
        }
        finally
        {
            _connection.Close();
        }
    }
}
در اینجا کلاس پایه Mini ORM طراحی شده را ملاحظه می‌کنید. برای نمونه قسمت GetRecords آن مانند مباحث استاندارد ADO.NET است. فقط کار خواندن و همچنین نگاشت رکوردهای دریافت شده از بانک اطلاعاتی به شیء‌ایی از نوع T توسط AutoMapper انجام خواهد شد.


نحوه‌ی استفاده از کلاس پایه AdoMapper

در کدهای ذیل نحوه‌ی ارث بری از کلاس پایه AdoMapper و سپس استفاده از متدهای آن‌را ملاحظه می‌کنید:
public class UsersService : AdoMapper<User>, IUsersService
{
    public UsersService(string connectionString)
        : base(connectionString)
    {
    }
 
    public IEnumerable<User> GetAll()
    {
        using (var command = new SqlCommand("SELECT * FROM Users"))
        {
            return GetRecords(command);
        }
    }
 
    public User GetById(int id)
    {
        using (var command = new SqlCommand("SELECT * FROM Users WHERE Id = @id"))
        {
            command.Parameters.Add(new SqlParameter("id", id));
            return GetRecord(command);
        }
    }
}
در این مثال نحوه‌ی تعریف کوئری‌های پارامتری نیز در متد GetById به نحو متداولی مشخص شده‌است. کار نگاشت حاصل این کوئری‌ها به اشیاء دات نتی را AutoMapper انجام خواهد داد. نحوه‌ی کار نیز، نگاشت فیلد f1 به خاصیت f1 است (هم نام‌ها به هم نگاشت می‌شوند).


تعریف پروفایل مخصوص AutoMapper

ORMهای تمام عیار، کار نگاشت فیلدهای بانک اطلاعاتی را به خواص اشیاء دات نتی، به صورت خودکار انجام می‌دهند. در اینجا همانند روش‌های متداول کار با AutoMapper نیاز است این نگاشت را به صورت دستی یکبار تعریف کرد:
public class UsersProfile : Profile
{
    protected override void Configure()
    {
        this.CreateMap<IDataRecord, User>();
    }
 
    public override string ProfileName
    {
        get { return this.GetType().Name; }
    }
}
و سپس در ابتدای برنامه آن‌را به AutoMapper معرفی نمود:
Mapper.Initialize(cfg => // In Application_Start()
{
    cfg.AddProfile<UsersProfile>();
});


سفارشی سازی نگاشت‌های AutoMapper

فرض کنید کلاس Advertisement زیر، معادل است با جدول Advertisements بانک اطلاعاتی؛ با این تفاوت که در کلاس تعریف شده، خاصیت TitleWithOtherName تطابقی با هیچکدام از فیلدهای بانک اطلاعاتی ندارد. بنابراین اطلاعاتی نیز به آن نگاشت نخواهد شد.
public class Advertisement
{
    public int Id { set; get; }
    public string Title { get; set; }
    public string Description { get; set; }
    public int UserId { get; set; }
 
    public string TitleWithOtherName { get; set; }
}
برای رفع این مشکل می‌توان حین تعریف پروفایل مخصوص Advertisement، آن‌را سفارشی سازی نیز نمود:
public class AdvertisementsProfile : Profile
{
    protected override void Configure()
    {
        this.CreateMap<IDataRecord, Advertisement>()
            .ForMember(dest => dest.TitleWithOtherName,
                       options => options.MapFrom(src =>
                            src.GetString(src.GetOrdinal("Title"))));
    }
 
    public override string ProfileName
    {
        get { return this.GetType().Name; }
    }
}
در اینجا پس از تعریف نگاشت مخصوص کار با IDataRecordها، عنوان شده‌است که هر زمانیکه به خاصیت TitleWithOtherName رسیدی، مقدارش را از فیلد Title دریافت و جایگزین کن.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
سازگار کردن لینک‌های قدیمی یک سایت با ساختار جدید آن در ASP.NET MVC
اگر پیشتر سایتی را در آدرس مشخصی در اینترنت داشته‌اید و اکنون تنها نرم افزار آن تغییر کرده است، اما نحوه ارائه خدمات آن خیر، لازم است بتوانید شرایط ذیل را مدیریت کنید:
- موتورهای جستجو مدام اطلاعات قبلی خود را به روز می‌کنند. اگر آدرس قبلی مقاله‌ای در سایت شما http://site/year/month/day/title بوده، برای نمونه گوگل هر از چندگاهی مجددا به این آدرس مراجعه می‌کند تا حداقل مطمئن شود وجود خارجی دارد یا خیر (این نکته را از لاگ‌های خطای سایت استخراج کردم).
- سایت‌های زیادی هستند که پیشتر به سایت شما و مطالب آن لینک داده‌اند. نمی‌توانید از آن‌ها درخواست کنید لطفا بانک اطلاعاتی خود را به روز کنید.
- اگر فید قبلی سایت شما http://site/feeds/posts بوده و اکنون چیز دیگری است، باز هم نمی‌توانید از همه درخواست کنید اطلاعات خود را به روز کنند. عده‌ای اینکار را خواهند کرد و تعداد زیادی هم خیر.

برای مدیریت یک چنین مواردی می‌توان از امکانات مسیریابی موجود در ASP.NET MVC استفاده کرد؛ که نمونه‌ای عملی از آن‌را جهت سازگاری سایت جاری با هاست قبلی آن (بلاگر) در ادامه مطالعه خواهید نمود:

الف) سازگار سازی لینک‌های قدیمی برچسب‌های سایت با ساختار جدید آن
در بلاگر آدرس‌های برچسب‌ها، به صورت http://site/search/label/name تعریف شده است. در سایت جاری برچسب‌ها توسط کنترلر Tag مدیریت می‌شوند. برای هدایت آدرس‌های قدیمی (موجود در موتورهای جستجو یا ثبت شده در سایت‌هایی که به ما لینک داده‌اند) می‌توان از تعریف مسیریابی ذیل در فایل global.asax استفاده کرد:
routes.MapRoute(
                "old_bloger_tags_list", // Route name
                "search/label/{name}", // URL with parameters
                new { controller = "Tag", action = "Index", name = UrlParameter.Optional, area = "" } // Parameter defaults
            );
به این ترتیب به صورت خودکار تمامی آدرس‌های شروع شده با http://site/search/label پالایش شده و سپس قسمت name آن‌ها جدا سازی می‌شود. این نام به متدی به نام Index در کنترلر Tag که دارای پارامتری به نام name است ارسال خواهد شد.

ب) از دست ندادن خوانندگان قدیمی فیدهای سایت
دو نوع فید کلی در بلاگر وجود دارد: http://site/feeds/posts/default و http://site/feeds/comments/default؛ اما در سایت جاری فیدها توسط کنترلری به نام Feed ارائه می‌شوند. برای سازگار سازی آدرس‌های قدیمی و هدایت آن‌ها به صورت خودکار به کنترلر فید می‌توان از دو تعریف مسیریابی ذیل استفاده کرد:
routes.MapRoute(
                "old_bloger_posts_feeds_list", // Route name
                "feeds/posts/default", // URL with parameters
                new { controller = "Feed", action = "Posts", name = UrlParameter.Optional, area = "" } // Parameter defaults
            );

routes.MapRoute(
                "old_bloger_comments_feeds_list", // Route name
                "feeds/comments/default", // URL with parameters
                new { controller = "Feed", action = "Comments", name = UrlParameter.Optional, area = "" } // Parameter defaults
            );            
در اینجا دو آدرس ذکر شده به کنترلر Feed و متدهای Posts و Comments آن هدایت خواهند شد و به این نحو کاربران قدیمی سایت هیچگونه تغییری را احساس نکرده و باز هم فیدخوان‌های آن‌ها، بدون مشکل کار خواهند کرد.

ج) پردازش لینک‌های قدیمی مطالب سایت و هدایت آن‌ها به آدرس‌های جدید
این مورد اندکی مشکل‌تر از موارد قبلی است:
routes.MapRoute(
                "old_bloger_post_urls",
                "{yyyy}/{mm}/{title}",
                new { controller = "Post", action = "OldBloggerLinks" },
                new { yyyy = @"\d{4}", mm = @"\d{1,2}" }
            );
برای نمونه آدرس مقاله‌ای مانند http://site/2012/05/ef-code-first-15.html را درنظر بگیرید. سه قسمت سال، ماه و عنوان آن، حائز اهمیت هستند. این‌ها را در اینجا به کنترلر Post و متد OldBloggerLinks آن هدایت خواهیم کرد. همچنین برای سال و ماه آن نیز قید تعریف شده است. سال عددی 4 رقمی است و ماه عددی یک تا دو رقمی.
کدهای متد OldBloggerLinks را در اینجا مشاهده می‌کنید:
        public virtual ActionResult OldBloggerLinks(int yyyy, int mm, string title)
        {
            var oldUrl = string.Format(CultureInfo.InvariantCulture, "https://www.dntips.ir/{0}/{1}/{2}", yyyy, mm.ToString("00"), title);
            var blogPost = _blogPostsService.FindBlogPost(oldUrl);
            if (blogPost != null)
                return RedirectToActionPermanent(actionName: ActionNames.Index, controllerName: MVC.Post.Name,
                                                 routeValues: new { id = blogPost.Id, name = blogPost.Title.GetPostSlug() });
            return this.Redirect("/");
        }
در اینجا چون ساختار لینک‌ها کلا تغییر کرده است، ابتدا بر اساس پارامترهای دریافت شده، لینک قدیمی بازسازی می‌شود. سپس به بانک اطلاعاتی مراجعه شده و لینک قدیمی به همراه شماره مطلب مرتبط با آن یافت می‌شود (یک فیلد oldUrl برای مطالب قدیمی در بانک اطلاعاتی وجود دارد). در آخر هم به کمک متد  RedirectToActionPermanent آدرس رسیده به آدرس جدید مطلب در سایت ترجمه و هدایت خواهد شد. Permanent بودن آن برای به روز رسانی خودکار اطلاعات موتورهای جستجو مفید است.


نتیجه گیری
به کمک امکانات مسیریابی توکار ASP.NET MVC می‌توان ساختار قدیمی یک سایت را به ساختار جدید آن ترجمه کرد. به این ترتیب لینک‌های قدیمی ثبت شده در صدها سایت اینترنتی که به سایت ما اشاره می‌کنند، مجددا بدون مشکل قابل استفاده بوده و همچنین موتورهای جستجو نیز امکان به روز رسانی اطلاعات خود را خواهند یافت.

بازخوردهای دوره
نگاهی به SignalR Hubs
یک hub را داخل یک پروژه دیگر از نوع class library  قرار دادم و با ارجاع به یک کنسول که selfhost شده، میخواستم ازش استفاده کنم، کار نمیکند. به همین سادگی!
کد hub
[HubName("messageHub")]
    public class MessageHub : Hub
    {
        public void NotifyAllClients()
        {
            Clients.All.Notify();
        } 
    }
کلاس startup
public partial class Startup
    {
        public void Configuration(IAppBuilder appBuilder)
        {
            var hubConfiguration = new HubConfiguration()
            {
                EnableDetailedErrors = true
            };

            appBuilder.MapSignalR(hubConfiguration);

            appBuilder.UseCors(CorsOptions.AllowAll);


        }
    }
نقطه آغازین برنامه:
static void Main(string[] args)
        {
            const string baseAddress = "http://localhost:9000/"; // "http://*:9000/";

            using (var webapp = WebApp.Start<Startup>(baseAddress))
            {
                Console.WriteLine("Start app...");

                var hubConnection = new HubConnection(baseAddress);
                IHubProxy messageHubProxy = hubConnection.CreateHubProxy("messageHub");

                messageHubProxy.On("notify", () =>
                {
                    Console.WriteLine();
                    Console.WriteLine("Notified!");
                });

                hubConnection.Start().Wait();

                Console.WriteLine("Start signalr...");

                bool dontExit = true;
                while (dontExit)
                {
                    var key = Console.ReadKey();
                    if (key.Key == ConsoleKey.Escape) dontExit = false;

                    messageHubProxy.Invoke("NotifyAllClients");
                }

            }
        }
اگر کلاس hub را به داخل پروژه‌ی slefhost منتقل کنم، کار میکند. اما در یک class library دیگر خیر.
نگارش دات نت و ارجاعات همه یکسان است. dotnet 4.5 
آیا نکته ای جاانداخته شده در این نمونه کد؟