مطالب
طراحی و پیاده سازی زیرساختی برای تولید خودکار کد منحصر به فرد در زمان ثبت رکورد جدید

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


یک مثال واقعی

در زمان ثبت یک Task، کاربر می‌تواند به صورت دستی یک شماره منحصر به فرد را نیز وارد کند؛ در غیر این صورت سیستم به طور خودکار شماره‌ای را به رکورد در حال ثبت اختصاص خواهد داد. بررسی یکتایی این کد در صورت وارد کردن به صورت دستی، توسط اعتبارسنج مرتبط باید انجام گیرد؛ ولی در غیر این صورت، زیرساخت مورد نظر تضمین می‌کند که شماره یکتایی را ایجاد کند.

ایجاد یک قرارداد برای موجودیت‌های دارای شماره منحصر به فرد
public interface INumberedEntity
{
    string Number { get; set; }
}
با استفاده از این واسط می‌توان از تکرار یکسری از تنظیمات مانند تنظیم طول فیلد Number و همچنین ایجاد ایندکس منحصر به فرد برروی آن، به شکل زیر جلوگیری کرد.
foreach (var entityType in builder.Model.GetEntityTypes()
    .Where(e => typeof(INumberedEntity).IsAssignableFrom(e.ClrType)))
{
    builder.Entity(entityType.ClrType)
        .Property(nameof(INumberedEntity.Number)).IsRequired().HasMaxLength(50);

    if (typeof(IMultiTenantEntity).IsAssignableFrom(entityType.ClrType))
    {
        builder.Entity(entityType.ClrType)
            .HasIndex(nameof(INumberedEntity.Number), nameof(IMultiTenantEntity.TenantId))
            .HasName(
                $"UIX_{entityType.ClrType.Name}_{nameof(IMultiTenantEntity.TenantId)}_{nameof(INumberedEntity.Number)}")
            .IsUnique();
    }
    else
    {
        builder.Entity(entityType.ClrType)
            .HasIndex(nameof(INumberedEntity.Number))
            .HasName($"UIX_{entityType.ClrType.Name}_{nameof(INumberedEntity.Number)}")
            .IsUnique();
    }
}

ایجاد یک Entity برای نگهداری شماره قابل استفاده بعدی مرتبط با موجودیت‌ها
public class NumberedEntity : Entity, IMultiTenantEntity
{
    public string EntityName { get; set; }
    public long NextNumber { get; set; }
    
    public long TenantId { get; set; }
}

با تنظیمات زیر:
public class NumberedEntityConfiguration : IEntityTypeConfiguration<NumberedEntity>
{
    public void Configure(EntityTypeBuilder<NumberedEntity> builder)
    {
        builder.Property(a => a.EntityName).HasMaxLength(256).IsRequired().IsUnicode(false);
        builder.HasIndex(a => a.EntityName).HasName("UIX_NumberedEntity_EntityName").IsUnique();
        builder.ToTable(nameof(NumberedEntity));
    }
}

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

  • ایجاد Gap مابین شماره‌های تولید شده، که مدنظر ما نمی‌باشد. (با توجه به اینکه امکان ثبت دستی را هم داریم، ممکن است کاربر شماره‌ای را وارد کرده باشد که با آخرین شماره ثبت شده تعداد زیادی فاصله دارد که به خودی خود مشکل ساز نیست؛ ولی در زمان ثبت رکورد بعدی اگر به صورت خودکار ثبت شماره داشته باشد، قطعا آخرین شماره (بزرگترین) را که به صورت دستی وارد شده بود، از جدول دریافت خواهد کرد)


پیاده سازی یک PreInsertHook برای مقداردهی پراپرتی Number

internal class NumberingPreInsertHook : PreInsertHook<INumberedEntity>
{
    private readonly IUnitOfWork _uow;
    private readonly IOptions<NumberingConfiguration> _configuration;

    public NumberingPreInsertHook(IUnitOfWork uow, IOptions<NumberingConfiguration> configuration)
    {
        _uow = uow ?? throw new ArgumentNullException(nameof(uow));
        _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
    }

    protected override void Hook(INumberedEntity entity, HookEntityMetadata metadata)
    {
        if (!entity.Number.IsNullOrEmpty()) return;

        bool retry;
        string nextNumber;
        
        do
        {
            nextNumber = GenerateNumber(entity);
            var exists = CheckDuplicateNumber(entity, nextNumber);
            retry = exists;
            
        } while (retry);
        
        entity.Number = nextNumber;
    }

    private bool CheckDuplicateNumber(INumberedEntity entity, string nextNumber)
    {
       //...
    }

    private string GenerateNumber(INumberedEntity entity)
    {
       //...
    }
}

ابتدا بررسی می‌شود اگر پراپرتی Number مقداردهی شده‌است، عملیات مقداردهی خودکار برروی آن انجام نگیرد. سپس با توجه به اینکه ممکن است به صورت دستی قبلا شماره‌ای مانند Task_1000 وارد شده باشد و NextNumber مرتبط هم مقدار 1000 را داشته باشد؛ در این صورت به هنگام ثبت رکورد بعدی، با توجه به Prefix تنظیم شده، دوباره به شماره Task_1000 خواهیم رسید که در این مورد خاص با استفاده از متد CheckDuplicateNumber این قضیه تشخیص داده شده و سعی مجددی برای تولید شماره جدید صورت می‌گیرد.


بررسی متد GenerateNumber

private string GenerateNumber(INumberedEntity entity)
{
    var option = _configuration.Value.NumberedEntityOptions[entity.GetType()];

    var entityName = $"{entity.GetType().FullName}";

    var lockKey = $"Tenant_{_uow.TenantId}_" + entityName;

    _uow.ObtainApplicationLevelDatabaseLock(lockKey);

    var nextNumber = option.Start.ToString();

    var numberedEntity = _uow.Set<NumberedEntity>().AsNoTracking().FirstOrDefault(a => a.EntityName == entityName);
    if (numberedEntity == null)
    {
        _uow.ExecuteSqlCommand(
            "INSERT INTO [dbo].[NumberedEntity]([EntityName], [NextNumber], [TenantId]) VALUES(@p0,@p1,@p2)", entityName,
            option.Start + option.IncrementBy, _uow.TenantId);
    }
    else
    {
        nextNumber = numberedEntity.NextNumber.ToString();
        _uow.ExecuteSqlCommand("UPDATE [dbo].[NumberedEntity] SET [NextNumber] = @p0 WHERE [Id] = @p1 ",
            numberedEntity.NextNumber + option.IncrementBy, numberedEntity.Id);
    }

    if (!string.IsNullOrEmpty(option.Prefix))
        nextNumber = option.Prefix + nextNumber;
    
    return nextNumber;
}

ابتدا با استفاده از متد الحاقی ObtainApplicationLevelDatabaseLock یک قفل منطقی را برروی یک منبع مجازی (lockKey) در سطح نرم افزار از طریق sp_getapplock ایجاد می‌کنیم. به این ترتیب بدون نیاز به درگیر شدن با مباحث isolation level بین تراکنش‌های همزمان یا سایر مباحث locking در سطح row یا table، به نتیجه مطلوب رسیده و تراکنش دوم که خواهان ثبت Task جدید می‌باشد، با توجه به اینکه INumberedEntity می‌باشد، لازم است پشت این global lock صبر کند و بعد از commit یا rollback شدن تراکنش جاری، به صورت خودکار قفل منبع مورد نظر باز خواهد شد.

پیاده سازی متد مذکور به شکل زیر می‌باشد:

public static void ObtainApplicationLevelDatabaseLock(this IUnitOfWork uow, string resource)
{
    uow.ExecuteSqlCommand(@"EXEC sp_getapplock @Resource={0}, @LockOwner={1}, 
                @LockMode={2} , @LockTimeout={3};", resource, "Transaction", "Exclusive", 15000);
}

با توجه به اینکه ممکن است درون تراکنش جاری چندین نمونه از موجودیت‌های INumberedEntity در حال ذخیره سازی باشند و از طرفی Hook ایجاد شده به ازای تک تک نمونه‌ها قرار است اجرا شود، ممکن است تصور این باشد که اجرای مجدد sp مذکور مشکل ساز شود و در واقع به Lock خود برخواهد خورد؛ ولی از آنجایی که پارامتر LockOwner با "Transaction" مقداردهی می‌شود، لذا فراخوانی مجدد این sp درون تراکنش جاری مشکل ساز نخواهد بود. 

گام بعدی، واکشی NextNumber مرتبط با موجودیت جاری می‌باشد؛ اگر در حال ثبت اولین رکورد هستیم، لذا numberedEntity مورد نظر مقدار null را خواهد داشت و لازم است شماره بعدی را برای موجودیت جاری ثبت کنیم. در غیر این صورت عملیات ویرایش با اضافه کردن IncrementBy به مقدار فعلی انجام می‌گیرد. در نهایت اگر Prefix ای تنظیم شده باشد نیز به ابتدای شماره تولیدی اضافه شده و بازگشت داده خواهد شد.

ساختار NumberingConfiguration

public class NumberingConfiguration
{
    public bool Enabled { get; set; }

    public IDictionary<Type, NumberedEntityOption> NumberedEntityOptions { get; } =
        new Dictionary<Type, NumberedEntityOption>();
}
public class NumberedEntityOption
{
    public string Prefix { get; set; }
    public int Start { get; set; } = 1;
    public int IncrementBy { get; set; } = 1;
}

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

گام آخر: ثبت PreInsertHook توسعه داده شده و همچنین تنظیمات مرتبط با الگوی تولید شماره موجودیت‌ها

public static void AddNumbering(this IServiceCollection services,
    IDictionary<Type, NumberedEntityOption> options)
{
    services.Configure<NumberingConfiguration>(configuration =>
    {
        configuration.Enabled = true;
        configuration.NumberedEntityOptions.AddRange(options);
    });
    
    services.AddTransient<IPreActionHook, NumberingPreInsertHook>();
}

و استفاده از این متد الحاقی در Startup پروژه

services.AddNumbering(new Dictionary<Type, NumberedEntityOption>
{
    [typeof(Task)] = new NumberedEntityOption
    {
        Prefix = "T_",
        Start = 1000,
        IncrementBy = 5
    }
});

و موجودیت Task

public class Task : TrackableEntity, IAggregateRoot, INumberedEntity
{
    public const int MaxTitleLength = 256;
    public const int MaxDescriptionLength = 1024; 

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo; 
    public byte[] RowVersion { get; set; }
    public string Number { get; set; }
}

با خروجی‌های زیر

پ.ن ۱: در برخی از Domain‌ها نیاز به ریست کردن این شماره‌ها براساس یکسری فیلد موجود در موجودیت مورد نظر نیز مطرح می‌باشد. به عنوان مثال در یک سیستم انبارداری شاید براساس FiscalYear و در یک سیستم فروش با توجه به نحوه فروش (SaleType)، لازم باشد این ریست برای شماره‌های موجودیت «سفارش»، انجام پذیرد. در کل با کمی تغییرات می‌توان از این روش مطرح شده در چنین حالاتی نیز به عنوان یک ابزار شماره گذاری خودکار کمک گرفت.
پ.ن ۲: استفاده از امکانات  Sequence در Sql Server هم شاید اولین راه حلی باشد که به ذهن می‌رسد؛ ولی از آنجایی که از تراکنش‌ها پشتیبانی ندارد، مسئله Gap بین شماره‌ها پابرجاست و همچنین آزادی عملی را به این شکل که در مطلب مطرح شد، نداریم.
مطالب
بررسی ساختارهای جدید DateOnly و TimeOnly در دات نت 6
به همراه دات نت 6، دو ساختار داده‌ی جدید DateOnly و TimeOnly نیز معرفی شده‌اند که امکان کار کردن ساده‌تر با قسمت‌های فقط تاریخ و یا فقط زمان DateTime را میسر می‌کنند. این دو نوع جدید نیز همانند DateTime، از نوع struct هستند و بنابراین value type محسوب می‌شوند. در فضای نام System قرار گرفته‌اند و همچنین با نوع‌های date و time مربوط به SQL Server، سازگاری کاملی دارند.


روش استفاده از نوع DateOnly در دات نت 6

نوع‌های جدید معرفی شده، بسیار واضح هستند و مقصود از بکارگیری آن‌ها را به خوبی بیان می‌کنند. برای مثال اگر نیاز بود تاریخی را بدون در نظر گرفتن قسمت زمان آن معرفی کنیم، می‌توان از نوع DateOnly استفاده کرد؛ مانند تاریخ تولد، روزهای کاری و امثال آن. تا پیش از این برای معرفی یک چنین تاریخ‌هایی، عموما قسمت زمان DateTime را با 00:00:00.000 مقدار دهی می‌کردیم؛ اما دیگر نیازی به این نوع تعاریف نیست و می‌توان مقصود خود را صریح‌تر بیان کرد.
روش معرفی نمونه‌ای از آن با معرفی سال، ماه و روز است:
 var date = new DateOnly(2020, 04, 20);
و یا اگر خواستیم یک DateTime موجود را به DateOnly تبدیل کنیم، می‌توان به صورت زیر عمل کرد:
 var currentDate = DateOnly.FromDateTime(DateTime.Now);

همچنین در اینجا نیز همانند DateTime می‌توان از متدهای Parse و یا TryParse، برای تبدیل یک رشته به معادل DateOnly آن، کمک گرفت:
if (DateOnly.TryParse("28/09/1984", new CultureInfo("en-US"), DateTimeStyles.None, out var result))
{
   Console.WriteLine(result);
}
در یک چنین حالتی ذکر CultureInfo، دقت کار را افزایش می‌دهد؛ در غیراینصورت از CultureInfo ترد جاری برنامه استفاده خواهد شد که می‌تواند در سیستم‌های مختلف، متفاوت باشد.

و یا می‌توان توسط متد ParseExact، ساختار تاریخ دریافتی را دقیقا مشخص کرد:
DateOnly d1 = DateOnly.ParseExact("31 Dec 1980", "dd MMM yyyy", CultureInfo.InvariantCulture);  // Custom format
Console.WriteLine(d1.ToString("o", CultureInfo.InvariantCulture)); // "1980-12-31"  (ISO 8601 format)

در حین نمونه سازی DateOnly، امکان ذکر تقویم‌های خاص، مانند PersianCalendar نیز وجود دارد:
var persianCalendar = new PersianCalendar();
DateOnly d2 = new DateOnly(1400, 9, 6, persianCalendar);
Console.WriteLine(d2.ToString("d MMMM yyyy", CultureInfo.InvariantCulture));

در اینجا همچنین متدهایی مانند AddDays، AddMonths و AddYears نیز بر روی date مهیا کار می‌کنند:
var newDate = date.AddDays(1).AddMonths(1).AddYears(1)

یک نکته: برخلاف DateTime، نوع DateOnly به همراه DateTimeKind مانند Utc و امثال آن نیست و همواره DateTimeKind آن Unspecified است.


روش استفاده از نوع TimeOnly در دات نت 6

نوع و ساختار TimeOnly، قسمت زمان را به نحو صریحی مشخص می‌کند؛ مانند ساعتی که باید هر روز راس آن، آلارمی به صدا درآید و یا جلسه‌ای تشکیل شود و یا وظیفه‌ای صورت گیرد. سازنده‌ی آن overload‌های قابل توجهی را داشته و می‌تواند یکی از موارد زیر باشد:
public TimeOnly(int hour, int minute)
public TimeOnly(int hour, int minute, int second)
public TimeOnly(int hour, int minute, int second, int millisecond)
برای نمونه برای نمایش 10:30 صبح، می‌توان به صورت زیر عمل کرد:
var startTime = new TimeOnly(10, 30);
در اینجا قسمت ساعت، 24 ساعتی تعریف شده‌است. بنابراین برای نمونه، ساعت 1 عصر را باید به صورت 13 قید کرد:
var endTime = new TimeOnly(13, 00, 00);

و یا برای مثال می‌توان این نمونه‌ها را از هم کم کرد:
var diff = endTime - startTime;
خروجی این تفاوت محاسبه شده، بر حسب TimeSpan است:
Console.WriteLine($"Hours: {diff.TotalHours}");
و یا با استفاده از متد الحاقی ToTimeSpan می‌توان یک TimeOnly را به TimeSpan معادلی تبدیل نمود:
TimeSpan ts = endTime.ToTimeSpan();

برای تبدیل قسمت زمان DateTime به TimeOnly، می‌توان از متد FromDateTime به صورت زیر استفاده کرد:
var currentTime = TimeOnly.FromDateTime(DateTime.Now);
و یا اگر بخواهیم یک DateOnly را به DateTime تبدیل کنیم، می‌توان از متد الحاقی ToDateTime به همراه ذکر قسمت زمان آن بر حسب TimeOnly کمک گرفت:
DateTime dt = date.ToDateTime(new TimeOnly(0, 0));
Console.WriteLine(dt);

و در این حالت اگر خواستیم بررسی کنیم که آیا زمانی بین دو زمان دیگر واقع شده‌است یا خیر، می‌توان از متد IsBetween استفاده نمود:
 var isBetween = currentTime.IsBetween(startTime, endTime);
Console.WriteLine($"Current time {(isBetween ? "is" : "is not")} between start and end");

در اینجا امکان مقایسه این نمونه‌ها، توسط عملگرهایی مانند < نیز وجود دارد:
var startTime = new TimeOnly(08, 00);
var endTime = new TimeOnly(09, 00);
 
Console.WriteLine($"{startTime < endTime}");

اگر نیاز به تبدیل رشته‌ای به TimeOnly بود، می‌توان از متد ParseExact به همراه ذکر ساختار مدنظر، استفاده کرد:
TimeOnly time = TimeOnly.ParseExact("5:00 pm", "h:mm tt", CultureInfo.InvariantCulture);  // Custom format
Console.WriteLine(time.ToString("T", CultureInfo.InvariantCulture)); // "17:00:00"  (long time format)


عدم پشتیبانی System.Text.Json از نوع‌های جدید DateOnly و TimeOnly

فرض کنید رکوردی را به صورت زیر تعریف کرده‌ایم که از نوع‌های جدید DateOnly و TimeOnly، تشکیل شده‌است:
public record DataTypeTest(DateOnly Date, TimeOnly Time);
اگر سعی کنیم نمونه‌ای از آن را به JSON تبدیل کنیم:
var date = DateOnly.FromDateTime(DateTime.Now);
var time = TimeOnly.FromDateTime(DateTime.Now);
var test = new DataTypeTest(date, time);
var json = JsonSerializer.Serialize(test);
با استثنای زیر مواجه خواهیم شد:
Serialization and deserialization of 'System.DateOnly' instances are not supported.

برای رفع این مشکل می‌توان ابتدا تبدیلگر ویژه‌ی DateOnly و
    public class DateOnlyConverter : JsonConverter<DateOnly>
    {
        private readonly string _serializationFormat;

        public DateOnlyConverter() : this(null)
        { }

        public DateOnlyConverter(string? serializationFormat)
        {
            _serializationFormat = serializationFormat ?? "yyyy-MM-dd";
        }

        public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var value = reader.GetString();
            return DateOnly.ParseExact(value!, _serializationFormat, CultureInfo.InvariantCulture);
        }

        public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
            => writer.WriteStringValue(value.ToString(_serializationFormat));
    }
و سپس تبدیلگر ویژه‌ی TimeOnly را به صورت زیر تدارک دید:
    public class TimeOnlyConverter : JsonConverter<TimeOnly>
    {
        private readonly string _serializationFormat;

        public TimeOnlyConverter() : this(null)
        {
        }

        public TimeOnlyConverter(string? serializationFormat)
        {
            _serializationFormat = serializationFormat ?? "HH:mm:ss.fff";
        }

        public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var value = reader.GetString();
            return TimeOnly.ParseExact(value!, _serializationFormat, CultureInfo.InvariantCulture);
        }

        public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options)
            => writer.WriteStringValue(value.ToString(_serializationFormat));
    }
و به نحو زیر مورد استفاده قرار داد:
var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
jsonOptions.Converters.Add(new DateOnlyConverter());
jsonOptions.Converters.Add(new TimeOnlyConverter());
var json = JsonSerializer.Serialize(test, jsonOptions);
مطالب
روش های مختلف پردازش یک رشته و تبدیل آن به نوع داده تاریخ
DateTime در طبقه بندی سی شارپ، جزء Strcut Type‌ها قرار می‌گیرد . عمدتا از DateTime برای مدیریت تاریخ، زمان و یا تاریخ-زمان استفاده می‌شود. خیلی از اوقات ما نیاز داریم تا رشته‌ای را به نوع تاریخ تبدیل کنیم تا بتوانیم عملیات مختلفی، همچون محاسبه‌ی اختلاف دو تاریخ، روز هفته، روز ماه و غیره را بدست آوریم. در دات نت متد‌های مختلفی وجود دارند که جداسازی تاریخ را از یک رشته برای ما فراهم می‌کنند:
  • Convert.ToDateTime()
  • DateTime.Parse()
  • DateTime.ParseExact()
  • DateTime.TryParse()
  • DateTime.TryParseExact()
در این مطلب این متد‌ها و تفاوت آنها را بررسی می‌کنیم.

تابع ()Convert.ToDateTime 
این تابع یک رشته‌ی با فرمت مشخص را به تاریخ و زمان تبدیل می‌کند. overload‌ها مختلف این تابع را در بخش زیر مشاهده می‌کنید:
• ToDateTime(string value)
Value  : رشته‌ای از تاریخ و زمان است.
 • ToDateTime(string value,IFormatProvider provide)
Value  : رشته‌ای از تاریخ و زمان است.
Provider : اطلاعات فرهنگ مورد نظر را فراهم می‌کند.
CultureInfo culture = new CultureInfo("en-US");    
DateTime tempDate = Convert.ToDateTime("1/1/2010 12:10:15 PM", culture);
در اینجا en-us اطلاعاتی را درباره‌ی فرهنگ کشور آمریکا، ارائه می‌دهد. لیست کامل فرهنگ‌های موجود در net. را می‌توانید در اینجا مشاهده کنید.
اگر رشته‌ی ما تهی (null) نباشد، بصورت درونی متد ()DateTime.Parse فراخوانی و نتیجه‌ی آن بازگردانده می‌شود. اما در صورتیکه رشته‌ی ارسالی ما تهی باشد، مقدار بازگردانده شده مقدار DateTime.MinValue که برابر با 0001/1/1 می‌باشد، بازگردانده می‌شود.
نمونه‌ای از خروجی این تابع با ورودی‌های مختلف :
string datestr = null;
Console.WriteLine(Convert.ToDateTime(datestr));//0001-01-01T00:00:00
datestr = "wrong string";
Console.WriteLine(Convert.ToDateTime(datestr));
//Unhandled Exception: System.FormatException: 
//The string was not recognized as a valid DateTime. 
//There is an unknown word starting at index 0.
datestr = "Tue Dec 30,2015";
Console.WriteLine(Convert.ToDateTime(datestr));
//Unhandled Exception: System.FormatException: 
//String was not recognized as a valid DateTime.

تابع () DateTime.Parse :
این تابع یک رشته‌ی با فرمت مشخص را به تاریخ و زمان تبدیل می‌کند. دو overload این تابع را در زیر می‌بینیم:
• DateTime.Parse(string value)
Value  : رشته‌ای از تاریخ و زمان می‌باشد.
• DateTime.Parse(String value, IFormatProvider provider)
Value : رشته‌ای از تاریخ و زمان می‌باشد.
Provider : اطلاعات فرهنگ مورد نظر را فراهم می‌کند.
  • DateTime.Parse(String value, IFormatProvider provider, DateTypeStyles styles)
Value : رشته‌ای از تاریخ و زمان می‌باشد.
Provider : اطلاعات فرهنگ مورد نظر را فراهم می‌کند 
Styles  : از طریق این پارامتر، تنظیمات قالب بندی رشته‌ی دریافتی برای سفارشی سازی کردن عملیات پردازش تعریف می‌شود. فرض کنید می‌خواهید کلیه‌ی فضاهای خالی (Space) را که قبل و بعد از رشته‌ی تاریخ هستند و نه در داخل رشته‌ی تاریخ، در زمان جداسازی و پردازش در نظر نگیرید. این پارامتر می‌تواند در اینجا به کمک ما بیاید (DateTimeStyles.AllowWhiteSpaces). مشاهده‌ی لیست کامل این خصوصیت از اینجا.
اگر مقدار رشته تهی باشد، استثنای Null نمایش داده خواهد شد (ArgumentNullException ) و اگر فرمت تاریخ ورودی صحیح نباشد، با استثنای فرمت غیرمعتبر روبرو خواهیم شد (FormatException).
string datestr = null;
Console.WriteLine(DateTime.Parse(datestr));
//// Exception: Argument null exception
datestr = "wrong string";            
Console.WriteLine(DateTime.Parse(datestr));
//// Exception: The string was not recognized as a valid DateTime. 
//// There is an unknown word starting at index 0.  
datestr = "Tue Dec 30, 2015";
//Unhandled Exception: System.FormatException: 
//String was not recognized as a valid DateTime. 
Console.WriteLine(DateTime.Parse(datestr));  

تابع ()DateTime.ParsExact
این تابع رشته‌ای شامل تاریخ و زمان را به‌همراه فرهنگ ارسالی، دریافت و آن را تبدیل به نوع DateTime می‌کند. فرمت رشته‌ی ارسالی باید با فرمت استاندارد رشته‌ی تاریخ یکسان باشد. overload‌های مختلف این تابع را در زیر مشاهده می‌کنید:
 • DateTime.ParseExact(string value, string format, IFormatProvider provider)
Value : رشته‌ای از تاریخ و زمان می‌باشد.
Provider : اطلاعات فرهنگ مورد نظر را فراهم می‌کند.

• DateTime.ParseExact(string value, string format, IFormatProvider provider, DateTimeStyles style)
Value : رشته‌ای از تاریخ و زمان می‌باشد.
Format : فرمت مورد نظر برای نمایش تاریخ بعد از تبدیل را مشخص می‌کند.
Provider : اطلاعات فرهنگ مورد نظر را فراهم می‌کند.
Styles  : از طریق این پارامتر تنظیمات قالب بندی رشته برای سفارشی کردن عملیات جداسازی و پردازش تعریف می‌شود.
• DateTime.ParseExact(string value, string[] formats, IFormatProvider provider, DateTimeStyles style)
 
Value : رشته‌ای از تاریخ و زمان می‌باشد.
Format : فرمت مورد نظر برای نمایش تاریخ بعد از تبدیل را مشخص می‌کند. تفاوت این حالت با حالت قبل این است که لیستی از فرمت‌ها را قبول می‌کند و حداقل باید یک فرمت با رشته‌ی ارسالی قابل انطباق باشد.
Provider : اطلاعات فرهنگ مورد نظر را فراهم می‌کند.
Styles : از طریق این پارامتر تنظیمات قالب بندی رشته برای سفارشی کردن عملیات جداسازی تعریف می‌شود. 
اگر مقدار رشته تهی باشد، استثنای Null نمایش داده خواهد شد (ArgumentNullException) و اگر فرمت تاریخ ورودی صحیح نباشد، با استثنای فرمت غیرمعتبر روبرو خواهیم شد(FormatException).
فرمت رشته‌ی تاریخ حتما باید با فرمت نوع تاریخ منطبق باشد. به همین منظور حالت‌های مختلفی را در آرایه می‌توان پیش بینی کرد. بطور مثال ممکن است رشته‌ی ما بصورت "2012-2-1" و یا "2012/2/1" باشد. بنابر این فرمت‌های "MM/dd/yyyy" و "MM-dd-yyyy" را از طریق آرایه ارسال می‌کنیم.
برای درک بهتر این موضوع، کد‌های زیر را مشاهده کنید: 
string datestr = null;
CultureInfo provider = CultureInfo.InvariantCulture;
Console.WriteLine(DateTime.ParseExact(datestr, "mm/dd/yyyy", provider));
//Unhandled Exception: System.ArgumentNullException: 
//String reference not set to an instance of a String.
datestr = "wrong date";
Console.WriteLine(DateTime.ParseExact(datestr, "mm/dd/yyyy", provider));
//Unhandled Exception: System.FormatException: 
//String was not recognized as a valid DateTime
datestr = "Tue Dec 30, 2015";
Console.WriteLine(DateTime.ParseExact(datestr, "mm/dd/yyyy", provider));
//Unhandled Exception: System.FormatException: 
//String was not recognized as a valid DateTime.
datestr = "10-22-2015";
Console.WriteLine(DateTime.ParseExact(datestr, "MM-dd-yyyy", provider));
//30/07/1394 12:00:00 ق.ظ
datestr = "10-22-2015";
Console.WriteLine(DateTime.ParseExact(datestr, new string[]
    {"MM-dd-yyyy", "MM/dd/yyyy", "MM.dd.yyyy"}, provider, DateTimeStyles.None));
//30/07/1394 12:00:00 ق.ظ

تابع ()DateTime.TryParse
این تابع رشته‌ای شامل تاریخ و زمان را به‌همراه فرهنگ مشخصی، دریافت و آن را تبدیل به نوع DateTime می‌کند و یک مقدار bool را برای اعلان این موضوع که عملیات تبدیل با موفقیت انجام شده است یا خیر، باز می‌گرداند. فرمت رشته‌ی ارسالی باید با فرمت رشته‌ی تاریخ یکسان باشد. overload‌های مختلف این تابع را در زیر مشاهده می‌کنید:
 
• DateTime.TryParse (String value, out DateTime result)
Value : رشته‌ای از تاریخ و زمان می‌باشد.
Result : مقدار تاریخ را بعد از جداسازی در خود ذخیره می‌کند.

• DateTime.TryParse(String value, IFormatProvider provider, DateTimeStyles styles, out DateTime result)
Value : رشته‌ای از تاریخ و زمان می‌باشد.
Provider : اطلاعات فرهنگ مورد نظر را فراهم می‌کند.
Styles  : از طریق این پارامتر می‌توانیم قالب پارامتر ارسالی را مشخص کنیم (مثلا نادیده گرفتن فضاهای خالی).
Result : مقدار تاریخ را بعد از جداسازی در خود ذخیره می‌کند.
این تابع همیشه سعی می‌کند رشته‌ی تاریخ را جداسازی کند. در صورت موفقیت در عملیات جداسازی رشته، تاریخ معتبر را بر می‌گرداند و در غیر اینصورت میزان کوچکترین تاریخ یا همان MinValue را که قبلا در همین مطلب اشاره شد، باز می‌گرداند. در صورتی هم که رشته‌، null یا با فرمت رشته‌ای غیر معتبر باشد، همان مقدار DateTime.MinValue بازگردانده می‌شود. همیشه با کنترل مقدار بازگشتی صحت انجام عملیات را می‌توان متوجه شد (true موفقیت در عملیات جداسازی و false عدم موفقیت در عملیات جداسازی).
نکته‌ی مهم عدم پرتاب استثتاء در صورت عدم موفقیت در عملیات جداسازی می‌باشد.
string datestr = null;
            DateTime temp;
            Console.WriteLine(DateTime.TryParse(datestr, out temp));
            Console.WriteLine(temp);
            //False
            //0001 - 01 - 01T00: 00:00

            datestr = "wrong date";
            Console.WriteLine(DateTime.TryParse(datestr, out temp));
            Console.WriteLine(temp);
            //False
            //0001 - 01 - 01T00: 00:00

            datestr = "Tue Dec 30, 2015";
            Console.WriteLine(DateTime.TryParse(datestr, out temp));
            Console.WriteLine(temp);
            //False
            //0001 - 01 - 01T00: 00:00

تابع ()DateTime.TryParseExact
این تابع رشته‌ای شامل تاریخ و زمان را به‌همراه فرهنگ مشخصی دریافت و آن را تبدیل به نوع DateTime می‌کند. رشته‌ی ارسالی باید منطبق با فرمت تاریخ باشد. overload‌های مختلف این تابع را در زیر مشاهده می‌کنید:

• DateTime.ParseExact(string value, string format, IFormatProvider provider, DateTimeStyles style)
Value : رشته‌ای از تاریخ و زمان می‌باشد.
Format : فرمت مورد نظر را برای نمایش تاریخ بعد از تبدیل، مشخص می‌کند.
Provider : اطلاعات فرهنگ مورد نظر را فراهم می‌کند.
Styles  : از طریق این پارامتر می‌توانیم قالب پارامتر ارسالی را مشخص کنیم (مثلا نادیده گرفتن فضاهای خالی).

• DateTime.ParseExact(string value, string[] formats, IFormatProvider provider, DateTimeStyles style)
Value : رشته‌ای از تاریخ و زمان می‌باشد.
Format : فرمت مورد نظر برای نمایش تاریخ بعد از تبدیل را مشخص می‌کند و می‌توان آرایه‌ای از فرمت‌ها را در اینجا ارسال کرد.
Provider : اطلاعات فرهنگ مورد نظر را فراهم می‌کند.
Styles  : از طریق این پارامتر می‌توانیم قالب پارامتر ارسالی را مشخص کنیم (مثلا نادیده گرفتن فضاهای خالی).
این تابع را در شرایط زیر، مقدار (MinValue( 1/1/0001 12:00:00 AM را باز می‌گرداند:
• رشته null  باشد.
• رشته خالی باشد "".
• فرمت رشته‌ی تاریخ غلط باشد.
• رشته با فرمت معرفی شده‌ی در provider، انطباق نداشته باشد.
• تنها در صورتی که مقدار انتخابی DateTimeStyle معتبر نباشد، استثنایی رخ می‌دهد.
همچنین همیشه پس از انجام عملیات، مقداری بولین را برای نمایش موفقیت یا عدم موفقیت جدا سازی، باز می‌گرداند.
  string datestr = null;
            DateTime temp;
            CultureInfo provider = CultureInfo.InvariantCulture;
            Console.WriteLine(DateTime.TryParseExact(datestr, "MM/dd/yyyy", provider, DateTimeStyles.None, out temp));
            Console.WriteLine(temp);
            //False
            //1/1/0001 12:00:00 AM

            datestr = "wrong date";
            Console.WriteLine(DateTime.TryParseExact(datestr, "MM/dd/yyyy", provider, DateTimeStyles.None, out temp));
            Console.WriteLine(temp);
            //False
            //1/1/0001 12:00:00 AM

            datestr = "Tue Dec 30, 2015";
            Console.WriteLine(DateTime.TryParseExact(datestr, "MM/dd/yyyy", provider, DateTimeStyles.None, out temp));
            Console.WriteLine(temp);
            //False
            //1/1/0001 12:00:00 AM

            datestr = "10‐22‐2015";
            Console.WriteLine(DateTime.TryParseExact(datestr, "MM/dd/yyyy", provider, DateTimeStyles.None, out temp));
            Console.WriteLine(temp);
            //False
            //1/1/0001 12:00:00 AM

            datestr = "10‐22‐2015";
            Console.WriteLine(DateTime.TryParseExact(
            datestr, "MM‐dd‐yyyy", provider, DateTimeStyles.None, out temp));
            Console.WriteLine(temp);
            //True
            //30/07/1394 12:00:00 ق.ظ

            datestr = "10‐12‐2015";
            Console.WriteLine(DateTime.TryParseExact(datestr, new string[] { "MM/dd/yyyy", "MM‐dd‐yyyy", "MM.dd.yyyy" }, provider, DateTimeStyles.None, out temp));
            Console.WriteLine(temp);
            //True
            //20/07/1394 12:00:00 ق.ظ


آنالیز متد‌های معرفی شده 
نوع DateTime متد‌های مختلفی را برای جداسازی رشته و تبدیل آن به تاریخ دارد. تفاوت‌های این متد‌ها را در بخش زیر بررسی می‌کنیم :

تفاوت Parse و ConvertToDateTime:
این دو متد شبیه به هم هستند، اما چند تفاوت کوچک با هم دارند:
• اگر مقدار رشته‌ی ارسالی null باشد، متد Parse، یک استثناء را ارسال می‌کند و متد ConvertToDateTime مقدار MinValue را باز می‌گرداند.
• در متد Parse می‌توان یک پارامتر اضافه‌تر نیز ارسال کرد که DateTimeStyle نامیده می‌شود. اما متد ConvertToDateTime این قابلیت را ندارد.
• تابع ConvertToDateTime به‌صورت داخلی از متد DateTime.Parse به‌همراه فرهنگ جاری استفاده می‌کند.

تفاوت تابع Parseو ParseExact:
این دو تابع شبیه به هم هستند، اما می‌توان یک پارامتر اضافی format را نیز به تابع ParseExact ارسال کرد. این پارامتر به ما کمک می‌کند تا رشته‌ای را که فرمت متفاوتی از حالت معمولی دارد، به تاریخ تبدیل کنیم. مثلا اگر رشته‌ی "11232015" بدین شکل باشد، فرمت باید به شکل "MMddyyyy" تعریف شود.

تفاوت تابع Parse و TryParse:
این دو متد شبیه به هم هستند، با این تفاوت که تابع TryParse در صورت عدم موفقیت جداسازی رشته، استثنائی را ارسال نمی‌کند و همیشه مقدار MinValue را در صورت عدم موفقیت در تبدیل، باز می‌گرداند.

تفاوت تابع TryParse و TryParseExact:
هر دو تابع شبیه به هم هستند؛ بجز در پارامتر format. تابع TryParseExact از یک پارامتر اضافه‌ی format استفاده می‌کند. ولی تابع TryParse اگر نتواند فرمت مورد نیاز را فراهم کند، مقدار minValue را باز می‌گرداند.
 
نظرات مطالب
بومی سازی منابع در پروژه‌های ASP.NET Core Web API
بر اساس اهدافی که این مطلب دنبال می‌کند، مشکلی مشاهده نشد. (هدف از این مطلب هم فقط استفاده از یک منبع اشتراکی هست وکار با Web API (ذکر شده در عنوان مطلب و مقدمه‌ی مطلب)؛ که اساسا ویژگی Display برای آن معنایی ندارد و مخصوص MVC هست و صفحات Razor. همچنین اگر قرار باشد دوبار این تعریف نام خاصیت انجام شود (یکبار پارامتری و یکبار در ویژگی Display)، اساسا چه نیازی به تعریف دوم هست؟ همان نام را بجای پارامتر قرار دادن بهتر هست و پارامتری کار کردن در اینجا هیچ مزیتی ندارد)
مدل مثال جاری به این صورت تغییر کرد: ( مثال کامل Core3xSharedResource-V2.zip )
using System.ComponentModel.DataAnnotations;

namespace Core3xSharedResource.Models.Account
{
    public class RegisterModel
    {
        [Required(ErrorMessage = "Please enter an email address")] // -->> from the shared resources
        [EmailAddress(ErrorMessage = "Please enter a valid email address")] // -->> from the shared resources
        public string Email { get; set; }

        [Required(ErrorMessage = "The {0} field is required")]
        [Display(Name = "User Name")]
        public string UserName { get; set; }
    }
}
نام خاصیت یکبار پارامتری تعریف شده و یکبار قرار است از ویژگی Display دریافت شود (یک کار اضافی و بی‌مفهوم در Web API که Viewهای آن قرار است توسط React یا Angular تامین شوند و نه الزاما توسط صفحات Razor که می‌توانند با ویژگی Display کار کنند)؛ با این مداخل متناظر جدید در فایل منبع اشتراکی:
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="Please enter an email address" xml:space="preserve">
    <value>لطفا ایمیلی را وارد کنید</value>
  </data>
  <data name="Please enter a valid email address" xml:space="preserve">
    <value>لطفا ایمیل معتبری را وارد کنید</value>
  </data>
  <data name="The {0} field is required" xml:space="preserve">
    <value>{0} را تکمیل کنید</value>
  </data>
  <data name="User Name" xml:space="preserve">
    <value>نام کاربری</value>
  </data>
</root>
پس از ارسال یک Post خالی به سمت اکشن متد یکی از کنترلرهای مثال، این خروجی کاملا بومی سازی شده قابل مشاهده‌است:

ضمن اینکه اگر کسی بخواهد کار جدی اعتبارسنجی را در Web API انجام دهد بهتر است از Fluent Validation استفاده کند (که تبدیل به یک استاندارد برای آن شده‌است).

مطالب
افزودن تصدیق ایمیل به ASP.NET Identity در MVC 5
در پست قبلی نحوه سفارشی کردن پروفایل کاربران در ASP.NET Identity را مرور کردیم. اگر بیاد داشته باشید یک فیلد آدرس ایمیل به کلاس کاربر اضافه کردیم. در این پست از این فیلد استفاده میکنیم تا در پروسه ثبت نام ایمیل‌ها را تصدیق کنیم. بدین منظور پس از ثبت نام کاربران یک ایمیل فعالسازی برای آنها ارسال می‌کنیم که حاوی یک لینک است. کاربران با کلیک کردن روی این لینک پروسه ثبت نام خود را تایید می‌کنند و می‌توانند به سایت وارد شوند. پیش از تایید پروسه ثبت نام، کاربران قادر به ورود نیستند.


در ابتدا باید اطلاعات کلاس کاربر را تغییر دهید تا دو فیلد جدید را در بر گیرد. یک فیلد شناسه تایید (confirmation token) را ذخیره می‌کند، و دیگری فیلدی منطقی است که مشخص می‌کند پروسه ثبت نام تایید شده است یا خیر. پس کلاس ApplicationUser  حالا باید بدین شکل باشد.
public class ApplicationUser : IdentityUser
{
    public string Email { get; set; }
    public string ConfirmationToken { get; set; }
    public bool IsConfirmed { get; set; }
}
اگر پیش از این کلاس ApplicationUser را تغییر داده اید، باید مهاجرت‌ها را فعال کنید و دیتابیس را بروز رسانی کنید. حالا می‌توانیم از این اطلاعات جدید در پروسه ثبت نام  استفاده کنیم و برای کاربران ایمیل‌های تاییدیه را بفرستیم.
private string CreateConfirmationToken()
{
    return ShortGuid.NewGuid();
}
 
private void SendEmailConfirmation(string to, string username, string confirmationToken)
{
    dynamic email = new Email("RegEmail");
    email.To = to;
    email.UserName = username;
    email.ConfirmationToken = confirmationToken;
    email.Send();
}
 
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        string confirmationToken = CreateConfirmationToken();
        var user = new ApplicationUser()
        {
            UserName = model.UserName,
            Email = model.Email,
            ConfirmationToken = confirmationToken, 
                IsConfirmed = false };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            SendEmailConfirmation(model.Email, model.UserName, confirmationToken);
            return RedirectToAction("RegisterStepTwo", "Account");
        }
        else
        {
            AddErrors(result);
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}
برای تولید شناسه‌های تایید (tokens) از کلاسی بنام ShortGuid استفاده شده است. این کلاس یک مقدار GUID را encode می‌کند که در نتیجه آن مقدار خروجی کوتاه‌تر بوده و برای استفاده در URL‌ها ایمن است. کد این کلاس را از این وبلاگ گرفته ام. پس از ایجاد حساب کاربری باید شناسه تولید شده را به آن اضافه کنیم و مقدار فیلد IsConfirmed را به false تنظیم کنیم. برای تولید ایمیل‌ها من از Postal استفاده می‌کنم. Postal برای ساختن ایمیل‌های دینامیک شما از موتور Razor استفاده می‌کند. می‌توانید ایمیل‌های ساده (plain text) یا HTML بسازید، عکس و فایل در آن درج و ضمیمه کنید و امکانات بسیار خوب دیگر. اکشن متد RegisterStepTwo تنها کاربر را به یک View هدایت می‌کند که پیامی به او نشان داده می‌شود.
بعد از اینکه کاربر ایمیل را دریافت کرد و روی لینک تایید کلیک کرد به اکشن متد RegisterConfirmation باز می‌گردیم.
private bool ConfirmAccount(string confirmationToken)
{
    ApplicationDbContext context = new ApplicationDbContext();
    ApplicationUser user =  context.Users.SingleOrDefault(u => u.ConfirmationToken == confirmationToken);
    if (user != null)
    {
        user.IsConfirmed = true;
        DbSet<ApplicationUser> dbSet = context.Set<ApplicationUser>();
        dbSet.Attach(user);
        context.Entry(user).State = EntityState.Modified;
        context.SaveChanges();
 
        return true;
    }
    return false;
}
 
[AllowAnonymous]
public ActionResult RegisterConfirmation(string Id)
{
    if (ConfirmAccount(Id))
    {
        return RedirectToAction("ConfirmationSuccess");
    }
    return RedirectToAction("ConfirmationFailure");
}
متد ConfirmAccount سعی می‌کند کاربری را در دیتابیس پیدا کند که شناسه تاییدش با مقدار دریافت شده از URL برابر است. اگر این کاربر پیدا شود، مقدار خاصیت IsConfirmed را به true تغییر می‌دهیم و همین مقدار را به تابع باز می‌گردانیم. در غیر اینصورت false بر می‌گردانیم. اگر کاربر تایید شده است، می‌تواند به سایت وارد شود. برای اینکه مطمئن شویم کاربران پیش از تایید ایمیل شان نمی‌توانند وارد سایت شوند، باید اکشن متد Login را کمی تغییر دهیم.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        var user = await UserManager.FindAsync(model.UserName, model.Password);
        if (user != null && user.IsConfirmed)
        {
            await SignInAsync(user, model.RememberMe);
            return RedirectToLocal(returnUrl);
        }
        else
        {
            ModelState.AddModelError("", "Invalid username or password.");
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}
تنها کاری که می‌کنیم این است که به دنبال کاربری می‌گردیم که فیلد IsConfirmed آن true باشد. اگر مقدار این فیلد false باشد کاربر را به سایت وارد نمی‌کنیم و پیغام خطایی نمایش می‌دهیم.
همین. این تمام چیزی بود که برای اضافه کردن تصدیق ایمیل به اپلیکیشن خود نیاز دارید. از آنجا که سیستم ASP.NET Identity با Entity Framework مدیریت می‌شود و با مدل Code First ساخته شده، سفارشی کردن اطلاعات کاربران و سیستم عضویت ساده‌تر از همیشه است.



توضیحاتی درباره کار با Postal

اگر به متد SendEmailConfirmation دقت کنید خواهید دید که آبجکتی از نوع Email می‌سازیم (که در اسمبلی‌های Postal وجود دارد) و از آن برای ارسال ایمیل استفاده می‌کنیم. عبارت "RegEmail" نام نمایی است که باید برای ساخت ایمیل استفاده شود. این متغیر از نوع dynamic است، مانند خاصیت ViewBag. بدین معنا که می‌توانید مقادیر مورد نظر خود را بصورت خواص دینامیک روی این آبجکت تعریف کنید. از آنجا که Postal از موتور Razor استفاده می‌کند، بعدا در View ایمیل خود می‌توانید به این مقادیر دسترسی داشته باشید.
در پوشه Views پوشه جدیدی بنام Emails بسازید. سپس یک فایل جدید با نام RegEmail.cshtml در آن ایجاد کنید. کد این فایل را با لیست زیر جایگزین کنید.
To: @ViewBag.To
From: YOURNAME@gmail.com
Subject: Confirm your registration

Hello @ViewBag.UserName,
Please confirm your registration by following the link bellow.

@Html.ActionLink(Url.Action("RegisterConfirmation", "Account", new { id = @ViewBag.ConfirmationToken }), "RegisterConfirmation", "Account", new { id = @ViewBag.ConfirmationToken }, null)
این فایل، قالب ایمیل‌های شما خواهد بود. ایمیل‌ها در حال حاظر بصورت plain text ارسال می‌شوند. برای اطلاعات بیشتر درباره ایمیل‌های HTML و امکانات پیشرفته‌تر به سایت پروژه Postal  مراجعه کنید.

همانطور که مشاهده می‌کنید در این نما همان خاصیت‌های دینامیک تعریف شده را فراخوانی می‌کنیم تا مقادیر لازم را بدست آوریم.

  • ViewBag.To آدرس ایمیل گیرنده را نشان می‌دهد.
  • ViewBag.UserName نام کاربر جاری را نمایش می‌دهد.
  • ViewBag.ConfirmationToken شناسه تولید شده برای تایید کاربر است.
در این قالب لینکی به متد RegisterConfirmation در کنترلر Account وجود دارد که شناسه تایید را نیز با پارامتری بنام id انتقال می‌دهد.

یک فایل ViewStart.cshtml_ هم در این پوشه بسازید و کد آن را با لیست زیر جایگزین کنید.
@{ Layout = null; /* Overrides the Layout set for regular page views. */ }
مطالب
خلاصه‌ای کاربردی در مورد Observable collection

Observable collection در WPF را می‌توان نوعی لیست جنریک ویژه تعریف کرد که زمانیکه به کنترلی بایند شد، کنترل را از تغییرات خودش آگاه می‌کند. برای مثال اگر آیتمی به این لیست اضافه شد بلافاصله آن آیتم را در کنترل مقید به آن نیز خواهید دید، به همین ترتیب در مورد ویرایش و یا حذف یک آیتم، بدون نیاز به کوچکترین تماسی با کنترل مورد نظر. برای مثال اگر مقدار یک خاصیت را تغییر دادید، بلافاصله بدون اینکه به کنترل مقید به آن اعلام کنیم که لطفا این مورد ویژه را برای من تغییر بده، شاهد نتیجه‌ی نهایی خواهیم بود.



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

الف) اصلا Observable collection چیست؟ چکار می‌کند؟
List vs ObservableCollection vs INotifyPropertyChanged in Silverlight

ب) نمی‌توانم از این مجموعه‌ی اشیای خودآگاه سازنده در یک ترد استفاده کنم. مشکل کجاست؟
این روزها نمی‌توان یک برنامه‌ی دسکتاپ خوب را بدون استفاده از تردها متصور شد. اما به محض سعی در به روز رسانی این لیست جنریک در یک ترد دیگر (ترد دیگر منظور هر تردی بجز ترد اصلی برنامه است که کار مدیریت رابط کاربر را به عهده دارد) خطای زیر ظاهر می‌شود:
This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread

راه حل:
Adding to an ObservableCollection from a background thread

ج) یکی از خاصیت‌های یک شیء این لیست جنریک ویژه را تغییر داده‌ام. اما هیچ تغییری در کنترل بایند شده به آن مشاهده نمی‌کنم. مشکل در کجاست؟
راه حل: پیاده سازی اینترفیس INotifyPropertyChanged را فراموش کرده‌اید:
Data Binding in WPF with the Monostate Pattern

د) خوب، این که خیلی دردسر دارد! راه ساده‌تری برای تعریف این موارد نیست؟!
هوشمندانه‌ترین روشی که برای حل این مساله تابحال دیده‌ام:
An easier way to manage INotifyPropertyChanged

ه) زمانیکه این یک لیست جنریک خودآگاه سازنده را به یک مثلا listview بایند می‌کنم، دیگر نمی‌توانم با استفاده از متد clear items آن کنترل، نسبت به خالی کردن نمای ظاهری آن اقدام کنم. چکار باید کرد؟
خطای مشاهده شده:
Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead

راه حل:
همان Observable collection اصلی را تخلیه کنید، UI به صورت خودکار به روز خواهد شد.

و) اضافه کردن رنجی از اطلاعات به آن به صورتی یکباره ممکن است کند باشد. چه باید کرد؟
راه حل:
AddRange for ObservableCollection in Silverlight 3


نظرات مطالب
معرفی افزونه‌ی WhySharper
ببخشید خوب دقت نکردم مشکل "Your client does not have permission " بود متاسفانه....
بازخوردهای پروژه‌ها
خطا در اجرای پروژه
سلام
در اجرای پروژه خطا دارم.
 Cannot open database "DecisionDB" requested by the login. The login failed.
Login failed for user 'xxxxx'.
Description:  An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. 

Line 172:
Line 173: foreach (var role in from record in standardRoles
Line 174: let role = this.FindByName(record.RoleName)
Line 175: where role == null
Line 176: select new Role

Source File:  C:\Users\Majid\Desktop\Decision-master\src\Decision.ServiceLayer\EFServiecs\Users\ApplicationRoleManager.cs   Line:  174
کانکشن استرینگ رو در فایل وب کانفیگ به شکل زیر قرار دادم:
 <add name="DefaultConnection" connectionString="Data Source=.;Initial Catalog=DecisionDB;Integrated Security = true" providerName="System.Data.SqlClient"/>