فارسی-سازی-خطاهای-Asp.Net-Identity-Model-با-LocalizationReviewed by کارشناسان.نت on September 17Rating:5

این مقاله از سورس کد مقاله قبلی استفاده می کند , لذا شما برای درک این مقاله , حتما" باید مقاله قبلی را مطالعه کنید .لینک دانلود سورس درپایان مقاله می باشد .

Localization و Globalization دو کلمه ایی می باشند , که برای چند فرهنگی کردن (Internationalization) یک وبسایت به کار می روند , Localization فرایندی است که برای شخصی کردن یک وبسایت برای یک فرهنگ خاص می باشد و Globalization فرایندی است برای تبدیل یک وبسایت به یک وبسایت چند فرهنگی .

 بر روی اینترنت در این باب مقالات زیادی موجود است , از جمله مقاله فارسی هفت قسمتی Globalization در ASP.NET MVC و اعتبارسنجی سایتهای چند زبانه در ASP.NET MVC - قسمت اول و دو مقاله بسیار عالی البته به زبان انگلیسی با نام های ASP.NET MVC 5 Internationalization و ASP.NET MVC 5 Internationalization · How to Store Strings in a Database or Xml . در این مقاله توضیحات جالبی راجع به زبانهای از راست به چپ مثل عربی و فارسی و ساخت اینترفیس انتخاب زبان توسط کاربر وجود دارد .

در این مقاله هدف خاص , فارسی سازی خطاهای Asp.Net Identity Model و مقداری هم Asp.Net MVC 5.0 می باشد و Internationalization هدف ما نمی باشد . در Asp.Net Identity Model 2.2.1 هنوز امکان استفاده ساده از Resource ها وجود ندارد لذا به مقداری کد نویسی نیاز است که من در این مقاله توضیح خواهم داد .

 

در این مقاله من از دو Resource فایل استفاده کرده ام یکی به نام ErrorMessages.resx برای ذخیره خطاها به زبان انگلیسی و ErrorMessages.fa.resx برای خطاها به زبان فارسی , این دو فایل در یک پروژه جداگانه به نام Resources از نوع Class Library ذخیره شده اند . شما می توانید این دو فایل را در پروژه MVC خود هم ذخیره کنید , اما اگر بخواهید از این خطاهای فارسی در سایر پروژه های خود نیز استفاده کنید , بهتر است آنها را در یک پروژه جداگانه ذخیره کنید , پس مراحل کار عبارت است از :

 

1 - ایجاد یک پروژه جدید از نوع Class Library

 

2 - راست کلیک روی پروژه و Add New Item و اضافه کردن دو Resources File به نامهای ErrorMessages.resx و ErrorMessages.fa.resx

 

3 - هر فایل Resource را در ویژوال استدیو باز کنید و Access Modifire هر دو Resource را همانند شکل زیر Public نمایید :

 

اگر Access Modifire را Public نکنید , هنگامی که پروژه Resources را به پروژه MVC خود رفرنس می کنید , در پروژه MVC , به Resource ها دسترسی نخواهید داشت .

شما باید برای هر پیام خطا , یک نام انتخاب کنید در ادامه مقاله از این نام ها برای مقدار دهی ErrorMessageResourceName استفاده می شود. متن فارسی خطا به همراه نام خطا را باید در فایل ErrorMessages.fa.resx ذخیره کنید و متن انگلیسی به همراه نام خطا را باید در فایل ErrorMessages.resx ذخیره کنید . در اشکال زیر تصویر دو فایل کامل شده قرار گرفته است :

 

اولین مکانی که نیاز به فارسی کردن خطا ها دارد , فایل AccountViewModels.cs در فولدر Model در پروژه MVC می باشد .در این فایل شما باید تمام خطاهای خواص اعتبار سنجی (Validation Attribute) مثل Compare و StringLength و غیره را فارسی کنید , برای اینکار کافی است دو Property هر Attribute را تنظیم نمایید , این دو عبارتند از ErrorMessageResourceType و  ErrorMessageResourceName .

 مقدار ErrorMessageResourceType  برابر می شود با نوع فایل Resource خطا و مقدار ErrorMessageResourceName برابر می شود با نام خطا . برای نمونه تکه ایی از کد فایل AccountViewModels.cs در زیر قابل مشاهده است :

 

public class RegisterViewModel

{

[Required(ErrorMessageResourceType = typeof(Resources.ErrorMessages),

ErrorMessageResourceName = "Required")]

[EmailAddress(ErrorMessageResourceType = typeof(Resources.ErrorMessages),

ErrorMessageResourceName = "Email", ErrorMessage = null)]

[Display(Name = "Email")]

public string Email { get; set; }

[Required(ErrorMessageResourceType = typeof(Resources.ErrorMessages),

ErrorMessageResourceName = "Required")]

//[PropertyTooShort(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]

[StringLength(100, ErrorMessageResourceType = typeof(Resources.ErrorMessages),

ErrorMessageResourceName = "PropertyTooShort", MinimumLength = 6)]

[DataType(DataType.Password)]

[Display(Name = "Password")]

public string Password { get; set; }

[DataType(DataType.Password)]

[Display(Name = "Confirm password")]

[Compare("Password", ErrorMessageResourceType = typeof(Resources.ErrorMessages),

ErrorMessageResourceName = "PasswordConfrim")]

public string ConfirmPassword { get; set; }

}

 

خوب فارسی کردن کل خطاهای فایل AccountViewModels.cs گام بزرگی است اما کافی نیست , بعضی از پیام های خطا را نمی توان به کمک ErrorMessageResourceType و  ErrorMessageResourceName ترجمه کرد , برای نمونه خطای

'The value X is not valid for Y' . برای حل این مشکل در ASP.Net MVC باید دو کار انجام شود :

 

1 - دو خط کد زیر به متد Application_Start در فایل Global.asax اضافه گردد :

ClientDataTypeModelValidatorProvider.ResourceClassKey = "Resources.ErrorMessages";

DefaultModelBinder.ResourceClassKey = "Resources.ErrorMessages";

 

در قطعه کد بالا Resources.ErrorMessages ادرس فایل Resource در پروژه Resources می باشد .

2 - پیام های خطای FieldMustBeDate و FieldMustBeNumeric و PropertyValueInvalid و PropertyValueRequired باید در دو فایل Resource قرار گرفته و ترجمه شوند :

 

FieldMustBeDate The field {0} must be a date.
FieldMustBeNumeric The field {0} must be a number.
PropertyValueInvalid The value '{0}' is not valid for {1}.
PropertyValueRequired A value is required.

 

در تصاویر فایل Resource فارسی در ابتدای مقاله شما می توانید ترجمه این خطاها را مشاهده بفرمایید .

خوب هنوز کار فارسی سازی Asp.Net Identity Model تکمیل نشده است , اخرین مکانی که باید فارسی شود عبارت است از پیام های خطاهای اعتبارسنجی کلاس ApplicationUserManager موجود در فایل IdentityConfig.cs در فولدر App_Start در پروژه MVC , این پیام های خطا متعلق به دو اعتبار سنج UserValidator و PasswordValidator می باشند .

شما برای فارسی کردن پیام های این دو اعتبارسنج راهی ندارین مگر اینکه هر دو آنها را از اول تعریف کنید , اما چگونه ؟

جواب خیلی ساده است شما با نگاه کردن به سورس این دو کلاس UserValidator و PassWordValidator در وبسایت CodePlex متوجه خواهید شد که این دو کلاس وابستگی خارجی ندارند و شما می توانید آنها را در پروژه MVC خود با پیامهای فارسی بازنویسی کنید.

من برای این منظور یک فولدر در پروژه MVC با نام Localization ساخته و این دو کلاس را در آن پیاده سازی می کنم ,کد کلاس PassWordValidator به شکل زیر پیاده سازی مجدد می شود :

 

/// <summary>

/// Used to validate some basic password policy like length and number of non alphanumerics

/// </summary>

public class LocalizePasswordValidator : IIdentityValidator<string>

{

public void Initialize()

{

RequiredLength = 6;

RequireNonLetterOrDigit = true;

RequireDigit = true;

RequireLowercase = true;

RequireUppercase = true;

}

/// <summary>

/// Minimum required length

/// </summary>

public int RequiredLength { get; set; }

/// <summary>

/// Require a non letter or digit character

/// </summary>

public bool RequireNonLetterOrDigit { get; set; }

/// <summary>

/// Require a lower case letter ('a' - 'z')

/// </summary>

public bool RequireLowercase { get; set; }

/// <summary>

/// Require an upper case letter ('A' - 'Z')

/// </summary>

public bool RequireUppercase { get; set; }

/// <summary>

/// Require a digit ('0' - '9')

/// </summary>

public bool RequireDigit { get; set; }

/// <summary>

/// Ensures that the string is of the required length and meets the configured requirements

/// </summary>

/// <param name="item"></param>

/// <returns></returns>

public virtual Task<IdentityResult> ValidateAsync(string item)

{

if (item == null)

{

throw new ArgumentNullException("item");

}

var errors = new List<string>();

if (string.IsNullOrWhiteSpace(item) || item.Length < RequiredLength)

{

errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.ErrorMessages.PropertyTooShort, RequiredLength));

}

if (RequireNonLetterOrDigit && item.All(IsLetterOrDigit))

{

errors.Add(Resources.ErrorMessages.PasswordRequireNonLetterOrDigit);

}

if (RequireDigit && item.All(c => !IsDigit(c)))

{

errors.Add(Resources.ErrorMessages.PasswordRequireDigit);

}

if (RequireLowercase && item.All(c => !IsLower(c)))

{

errors.Add(Resources.ErrorMessages.PasswordRequireLower);

}

if (RequireUppercase && item.All(c => !IsUpper(c)))

{

errors.Add(Resources.ErrorMessages.PasswordRequireUpper);

}

if (errors.Count == 0)

{

return Task.FromResult(IdentityResult.Success);

}

return Task.FromResult(IdentityResult.Failed(String.Join(" ", errors)));

}

/// <summary>

/// Returns true if the character is a digit between '0' and '9'

/// </summary>

/// <param name="c"></param>

/// <returns></returns>

public virtual bool IsDigit(char c)

{

return c >= '0' && c <= '9';

}

/// <summary>

/// Returns true if the character is between 'a' and 'z'

/// </summary>

/// <param name="c"></param>

/// <returns></returns>

public virtual bool IsLower(char c)

{

return c >= 'a' && c <= 'z';

}

/// <summary>

/// Returns true if the character is between 'A' and 'Z'

/// </summary>

/// <param name="c"></param>

/// <returns></returns>

public virtual bool IsUpper(char c)

{

return c >= 'A' && c <= 'Z';

}

/// <summary>

/// Returns true if the character is upper, lower, or a digit

/// </summary>

/// <param name="c"></param>

/// <returns></returns>

public virtual bool IsLetterOrDigit(char c)

{

return IsUpper(c) || IsLower(c) || IsDigit(c);

}

}

 

در مورد این کلاس فقط یک نکته لازم به ذکر است و ان هم افزودن متد Initialize برای مقدار دهی اولیه این اعتبارسنج , ما دقیقا" یک متد با همین نام به کلاس UserValidator اضافه می کنیم تا مقدار دهی اولیه را انجام دهد , همانند کد زیر :

 

/// <summary>

/// Validates users before they are saved

/// </summary>

/// <typeparam name="TUser"></typeparam>

/// <typeparam name="TKey"></typeparam>

public class LocalizeUserValidator<TUser, TKey> : IIdentityValidator<TUser>

where TUser : class, IUser<TKey>

where TKey : IEquatable<TKey>

{

/// <summary>

/// Constructor

/// </summary>

/// <param name="manager"></param>

public LocalizeUserValidator(ApplicationUserManager manager)

{

if (manager == null)

{

throw new ArgumentNullException("manager");

}

AllowOnlyAlphanumericUserNames = true;

Manager = manager;

}

public void Initialize()

{

AllowOnlyAlphanumericUserNames = false;

RequireUniqueEmail = true;

}

/// <summary>

/// Only allow [A-Za-z0-9@_] in UserNames

/// </summary>

public bool AllowOnlyAlphanumericUserNames { get; set; }

/// <summary>

/// If set, enforces that emails are non empty, valid, and unique

/// </summary>

public bool RequireUniqueEmail { get; set; }

private ApplicationUserManager Manager { get; set; }

/// <summary>

/// Validates a user before saving

/// </summary>

/// <param name="item"></param>

/// <returns></returns>

public virtual async Task<IdentityResult> ValidateAsync(TUser item)

{

if (item == null)

{

throw new ArgumentNullException("item");

}

var errors = new List<string>();

await ValidateUserName(item, errors).WithCurrentCulture();

if (RequireUniqueEmail)

{

await ValidateEmailAsync(item, errors).WithCurrentCulture();

}

if (errors.Count > 0)

{

return IdentityResult.Failed(errors.ToArray());

}

return IdentityResult.Success;

}

private async Task ValidateUserName(TUser user, List<string> errors)

{

if (string.IsNullOrWhiteSpace(user.UserName))

{

errors.Add(String.Format(CultureInfo.CurrentCulture, ErrorMessages.PropertyTooShort, "Name"));

}

else if (AllowOnlyAlphanumericUserNames && !Regex.IsMatch(user.UserName, @"^[A-Za-z0-9@_\.]+$"))

{

// If any characters are not letters or digits, its an illegal user name

errors.Add(String.Format(CultureInfo.CurrentCulture, ErrorMessages.InvalidUserName, user.UserName));

}

else

{

var owner = await Manager.FindByNameAsync(user.UserName).WithCurrentCulture();

if (owner != null && !Object.Equals(owner.Id, user.Id))

{

errors.Add(String.Format(CultureInfo.CurrentCulture, ErrorMessages.DuplicateName, user.UserName));

}

}

}

// make sure email is not empty, valid, and unique

private async Task ValidateEmailAsync(TUser user, List<string> errors)

{

var email = user.UserName;

if (string.IsNullOrWhiteSpace(email))

{

errors.Add(String.Format(CultureInfo.CurrentCulture, ErrorMessages.PropertyTooShort, "Email"));

return;

}

try

{

var m = new MailAddress(email);

}

catch (FormatException)

{

errors.Add(String.Format(CultureInfo.CurrentCulture, ErrorMessages.InvalidEmail, email));

return;

}

//var owner = await Manager.FindByEmailAsync(email).WithCurrentCulture();

//if (owner != null && !EqualityComparer<TKey>.Default.Equals(owner.Id, user.Id))

//{

// errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.DuplicateEmail, email));

//}

}

}

 

در مورد قطعه کد بالا برای کلاس UserValidator  ذکر چند نکته ضروری است :

_ در مورد تکه کدی که در پایین کلاس کامنت شده است , به این علت است که ما از ایمیل به عنوان UserName یا نام کاربری استفاده می کنیم و یکبار تکراری نبودن نام کاربری را چک می کنیم , پس دیگر نیازی به چک کردن ایمیل نمی باشد . 

_ اما نکته مهم در این کلاس , استفاده از ApplicationUserManager به جای UserManager مباشد , علت این امر این است که ما در مقاله قبلی  توسعه مدل Asp.Net Identity 2.0 کلاس UserManager را با نام ApplicationUserManager توسعه داده ایم و لذا دیگر نیازی به استفاده از UserManager نمی باشد .

حال باید از این دو کلاس به شکل زیر در کلاس ApplicationUserManager استفاده کنیم :

 

public ApplicationUserManager(IUserStore<ApplicationUser, int> store, IDataProtectionProvider dataProtectionProvider, EmailService emailService, SmsService smsService)

: base(store)

{

UserValidator = new LocalizeUserValidator<ApplicationUser, int>(this)

{

AllowOnlyAlphanumericUserNames = false,

RequireUniqueEmail = true

};

// Configure validation logic for passwords

PasswordValidator = new LocalizePasswordValidator

{

RequiredLength = 6,

RequireNonLetterOrDigit = true,

RequireDigit = true,

RequireLowercase = true,

RequireUppercase = true,

};

 

این روش بدون استفاده از تزریق وابستگی می باشد , من در انتهای این مقاله این دو کلاس LocalizeUserValidator و LocalizePasswordValidator را به کمک AutoFac به کلاس ApplicationUserManager تزریق خواهم کرد .

 خوب خود کار فارسی کردن پیامهای خطا Asp.Net Identity Model به پایان رسیده است , حال باید به روشی از این خطاهای فارسی استفاده کرد , یک روش استفاده از یک Base Controller و فارسی کردن Culture وبسایت همانند شکل زیر می باشد :

public class BaseController : Controller

{

protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)

{

//string cultureName = null;

// Attempt to read the culture cookie from Request

//HttpCookie cultureCookie = Request.Cookies["_culture"];

//if (cultureCookie != null)

// cultureName = cultureCookie.Value;

//else

// cultureName = Request.UserLanguages != null && Request.UserLanguages.Length > 0 ? Request.UserLanguages[0] : null; // obtain it from HTTP header AcceptLanguages

// Validate culture name

//cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe

 

// Modify current thread's cultures

//Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);

//Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;

Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("fa");

 

return base.BeginExecuteCore(callback, state);

}

}

 

نکات قطعه کد بالا :

_ مواردی که کامنت شده است مربوط است به خواند Cultre از کوکی که توسط کاربر که در ابتدای ورود به سایت انتخاب شده است , برای توضیحات بیشتر راجع به چند زبانی می توانید مقاله ASP.NET MVC 5 Internationalization را مطالعه بفرمایید . ولی در وبسایت ما تنها زبان فارسی پشتیبانی می شود پس نیازی به چند زبانی و خواندن Cultre از کوکی نمی باشد .

_ روش دیگر به غیر از این BaseController این است که شما پیام های فارسی را به جای فایل ErrorMessages.fa.resx در فایل ErrorMessages.resx ذخیره کنید . چون فایل ErrorMessages.resx پیش فرض است نیازی به تغییر Cultre نمی باشد .

در نهایت تمام Controller های وبسایت شما باید از این BaseController ارث ببرد همانند شکل زیر :

[Authorize]

public class AccountController : BaseController

 

اگر شما از تزریق وابستگی در وبسایتتان استفاده نکرده ایید , این مقاله برای شما درهمین جا به پایان می رسد , اما اکر می خواهیید از تزریق وابستگی استفاده کنید , من در ادامه دو کلاس LocalizeUserValidator و LocalizePasswordValidator را به کمک AutoFac به کلاس ApplicationUserManager تزریق خواهم کرد . برای این منظور تنها کافی است , تعریف کلاس ApplicationUserManager به شکل زیر تغییر کند و مقدار دهی UserValidator و PasswordValidator  از درون این کلاس حذف شوند همانند شکل زیر :

public ApplicationUserManager(IUserStore<ApplicationUser, int> store, IDataProtectionProvider dataProtectionProvider, EmailService emailService, SmsService smsService, IIdentityValidator<string> localizePasswordValidator)

: base(store)

همانطور که مشاهده می کنید وابستگی PasswordValidator  تزریق شده است , اما برای UserValidator تزریقی وجود ندارد , در حقیقت چون PasswordValidator و UserValidator برای کلاس ApplicationUserManager یک Property به حساب می ایند با یک دستور AutoFac می توان آنها را تزریق کرد , یعنی در حقیقت شما حتی نیاز به نوشتن IIdentityValidator<string> localizePasswordValidator ندارید.

اما یک مشکل دیگر هم وجود دارد کلاس LocalizeUserValidator و ApplicationUserManager  به صورت دور به هم وابسته هستند یعنی به اصطلاح Circular Dependencies . نهایتا" در اخرین قدم برای حل این مشکلات و کامل شدن تزریق وابستگی ها , کلاس Startup به شکل زیر بازنویسی می شود :

[assembly: OwinStartupAttribute(typeof(OO.WebUI.Startup))]

namespace OO.WebUI

{

public partial class Startup

{

public void Configuration(IAppBuilder app)

{

var builder = new ContainerBuilder();

// REGISTER DEPENDENCIES

builder.RegisterType<ApplicationDbContext>().AsSelf().InstancePerRequest();

builder.RegisterType<SmsService>().AsSelf().InstancePerRequest();

builder.RegisterType<EmailService>().AsSelf().InstancePerRequest();

builder.RegisterType<LocalizeUserValidator<ApplicationUser, int>>().As<IIdentityValidator<ApplicationUser>>().InstancePerRequest().OnActivating(e => e.Instance.Initialize());

builder.RegisterType<LocalizePasswordValidator>().As<IIdentityValidator<string>>().InstancePerRequest().OnActivating(e => e.Instance.Initialize());

builder.RegisterType<ApplicationUserStore>().As<IUserStore<ApplicationUser, int>>().InstancePerRequest();

builder.RegisterType<ApplicationUserManager>().AsSelf().InstancePerRequest().PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies);

//builder.RegisterType<ApplicationUserManager>().As<UserManager<ApplicationUser, int>>().InstancePerRequest().PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies);

builder.RegisterType<ApplicationRoleStore>().As<IRoleStore<ApplicationRole, int>>().InstancePerRequest();

builder.RegisterType<ApplicationRoleManager>().AsSelf().InstancePerRequest();

builder.RegisterType<GroupStoreBase>().AsSelf().InstancePerRequest();

builder.RegisterType<ApplicationGroupStore>().AsSelf().InstancePerRequest();

builder.RegisterType<ApplicationGroupManager>().AsSelf().InstancePerRequest();

builder.RegisterType<ApplicationSignInManager>().AsSelf().InstancePerRequest();

builder.Register<IAuthenticationManager>(c => HttpContext.Current.GetOwinContext().Authentication).InstancePerRequest();

builder.Register<IDataProtectionProvider>(c => app.GetDataProtectionProvider()).InstancePerRequest();

// REGISTER CONTROLLERS SO DEPENDENCIES ARE CONSTRUCTOR INJECTED

builder.RegisterControllers(typeof(MvcApplication).Assembly);

// BUILD THE CONTAINER

var container = builder.Build();

// REPLACE THE MVC DEPENDENCY RESOLVER WITH AUTOFAC

DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

// REGISTER WITH OWIN

app.UseAutofacMiddleware(container);

app.UseAutofacMvc();

ConfigureAuth(app);

}

}

}

 

دستور PropertyWiringOptions.AllowCircularDependencies برای حل مشکل وابسنگی دایره وار به کار می رود و دستور e => e.Instance.Initialize برای فراخوانی متد Initialize برای مقدار دهی اولیه کلاس های LocalizeUserValidator و LocalizePasswordValidator به کار می رود برای توضیحات بیشتر به مستندات AutoFac  مراجعه بفرمایید .

بعد از کامل شدن تزریق وابستگی , متد Seed کلاس Configuration در فولدر ApplicationDbContextMigrations باید به شکل زیر بازنویسی گردد :

 

protected override void Seed(OO.WebUI.Models.ApplicationDbContext context)

{

var userManager = new ApplicationUserManager(new ApplicationUserStore(context), new DpapiDataProtectionProvider(), new EmailService(), new SmsService(),null);

userManager.UserValidator = new UserValidator<ApplicationUser, int>(userManager);

userManager.PasswordValidator = new PasswordValidator();

var roleManager = new ApplicationRoleManager(new ApplicationRoleStore(context));

const string name = "admin@example.com";

const string password = "Admin@123456";

const string roleName = "Admin";

 

 

//Create Role Admin if it does not exist

var role = roleManager.FindByName(roleName);

if (role == null)

{

role = new ApplicationRole(roleName);

var roleresult = roleManager.Create(role);

}

 

 

var user = userManager.FindByName(name);

if (user == null)

{

user = new ApplicationUser

{

UserName = name,

Email = name,

EmailConfirmed = true

};

var result = userManager.Create(user, password);

result = userManager.SetLockoutEnabled(user.Id, false);

}

 

 

var groupManager = new ApplicationGroupManager(context, userManager, roleManager, new ApplicationGroupStore(context, new GroupStoreBase(context)));

var newGroup = new ApplicationGroup("SuperAdmins", "Full Access to All");

 

 

groupManager.CreateGroup(newGroup);

groupManager.SetUserGroups(user.Id, new int[] { newGroup.Id });

groupManager.SetGroupRoles(newGroup.Id, new int[] { role.Id });

}

دو تا سورس کد برای دانلود وجود دارد , OnLineOstad.rar با حجم کمتر , چون Nuget Pakage هایش همراهش نیست , در نتیجه بعد از باز کردن Solution در ویژوال استدیو برای دریافت Missing Pakage ها باید یکبار Rebuild کنید و OnLineOstadByPackage.rar با حجم بیشتر به همراه Nuget هایش .امیدوارم این پروژه بتواند برای شما مفید باشد .