public async Task<IHttpActionResult> LoginUserAsync(LoginUserBindingModel model) { var requestParams = new List<KeyValuePair<string, string>> { new KeyValuePair<string, string>("grant_type", "password"), new KeyValuePair<string, string>("username", model.Username), new KeyValuePair<string, string>("password", model.Password) }; var requestParamsFormUrlEncoded = new FormUrlEncodedContent(requestParams); using (var client = new HttpClient()) { client.BaseAddress = new Uri("http://localhost:9577/"); client.DefaultRequestHeaders.Accept.Clear(); // client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var response = await client.PostAsync("/login", requestParamsFormUrlEncoded); if (response.StatusCode == HttpStatusCode.OK) { var responseString = await response.Content.ReadAsStringAsync(); var jsSerializer = new JavaScriptSerializer(); var responseData = jsSerializer.Deserialize<Dictionary<string, string>>(responseString); return Content(HttpStatusCode.OK, responseData); } else { return BadRequest(response.ToString()); } } }
خلاصه مهاجرت داده پروفایل ها
- کلاس جدیدی بسازید که دارای خواصی برای ذخیره اطلاعات پروفایل است.
- کلاس جدیدی بسازید که از 'ProfileBase' ارث بری میکند و متدهای لازم برای دریافت پروفایل کاربران را پیاده سازی میکند.
- استفاده از تامین کنندههای پیش فرض را، در فایل web.config فعال کنید. و کلاسی که در مرحله 2 ساختید را بعنوان کلاس پیش فرض برای خواندن اطلاعات پروفایل معرفی کنید.
شروع به کار
پوشه جدیدی با نام 'Models' بسازید تا اطلاعات پروفایل را در آن قرار دهیم.
بعنوان یک مثال، بگذارید تا تاریخ تولد کاربر، شهر سکونت، قد و وزن او را در پروفایلش ذخیره کنیم. قد و وزن بصورت یک کلاس سفارشی (custom class) بنام 'PersonalStats' ذخیره میشوند. برای ذخیره و بازیابی پروفایل ها، به کلاسی احتیاج داریم که 'ProfileBase' را ارث بری میکند. پس کلاس جدیدی با نام 'AppProfile' بسازید.
public class ProfileInfo { public ProfileInfo() { UserStats = new PersonalStats(); } public DateTime? DateOfBirth { get; set; } public PersonalStats UserStats { get; set; } public string City { get; set; } } public class PersonalStats { public int? Weight { get; set; } public int? Height { get; set; } } public class AppProfile : ProfileBase { public ProfileInfo ProfileInfo { get { return (ProfileInfo)GetPropertyValue("ProfileInfo"); } } public static AppProfile GetProfile() { return (AppProfile)HttpContext.Current.Profile; } public static AppProfile GetProfile(string userName) { return (AppProfile)Create(userName); } }
پروفایل را در فایل web.config خود فعال کنید. نام کلاسی را که در مرحله قبل ساختید، بعنوان کلاس پیش فرض برای ذخیره و بازیابی پروفایلها معرفی کنید.
<profile defaultProvider="DefaultProfileProvider" enabled="true" inherits="UniversalProviders_ProfileMigrations.Models.AppProfile"> <providers> ..... </providers> </profile>
برای دریافت اطلاعات پروفایل از کاربر، فرم وب جدیدی در پوشه Account بسازید و آنرا 'AddProfileData.aspx' نامگذاری کنید.
<h2> Add Profile Data for <%# User.Identity.Name %></h2> <asp:Label Text="" ID="Result" runat="server" /> <div> Date of Birth: <asp:TextBox runat="server" ID="DateOfBirth"/> </div> <div> Weight: <asp:TextBox runat="server" ID="Weight"/> </div> <div> Height: <asp:TextBox runat="server" ID="Height"/> </div> <div> City: <asp:TextBox runat="server" ID="City"/> </div> <div> <asp:Button Text="Add Profile" ID="Add" OnClick="Add_Click" runat="server" /> </div>
کد زیر را هم به فایل code-behind اضافه کنید.
protected void Add_Click(object sender, EventArgs e) { AppProfile profile = AppProfile.GetProfile(User.Identity.Name); profile.ProfileInfo.DateOfBirth = DateTime.Parse(DateOfBirth.Text); profile.ProfileInfo.UserStats.Weight = Int32.Parse(Weight.Text); profile.ProfileInfo.UserStats.Height = Int32.Parse(Height.Text); profile.ProfileInfo.City = City.Text; profile.Save(); }
دقت کنید که فضای نامی که کلاس AppProfile در آن قرار دارد را وارد کرده باشید.
اپلیکیشن را اجرا کنید و کاربر جدیدی با نام 'olduser' بسازید. به صفحه جدید 'AddProfileData' بروید و اطلاعات پروفایل کاربر را وارد کنید.
با استفاده از پنجره Server Explorer میتوانید تایید کنید که اطلاعات پروفایل با فرمت xml در جدول 'Profiles' ذخیره میشوند.
مهاجرت الگوی دیتابیس
اسکریپت مورد نیاز را از آدرس https://raw.github.com/suhasj/UniversalProviders-Identity-Migrations/master/Migration.txt دریافت کرده و آن را اجرا کنید. اگر اتصال خود به دیتابیس را تازه کنید خواهید دید که جداول جدیدی اضافه شده اند. میتوانید دادههای این جداول را بررسی کنید تا ببینید چگونه اطلاعات منتقل شده اند.
مهاجرت اپلیکیشن برای استفاده از ASP.NET Identity
- Microsoft.AspNet.Identity.EntityFramework
- Microsoft.AspNet.Identity.Owin
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.Facebook
- Microsoft.Owin.Security.Google
- Microsoft.Owin.Security.MicrosoftAccount
- Microsoft.Owin.Security.Twitter
using Microsoft.AspNet.Identity.EntityFramework; using System; using System.Collections.Generic; using System.Linq; using System.Web; using UniversalProviders_ProfileMigrations.Models; namespace UniversalProviders_Identity_Migrations { public class User : IdentityUser { public User() { CreateDate = DateTime.UtcNow; IsApproved = false; LastLoginDate = DateTime.UtcNow; LastActivityDate = DateTime.UtcNow; LastPasswordChangedDate = DateTime.UtcNow; Profile = new ProfileInfo(); } public System.Guid ApplicationId { get; set; } public bool IsAnonymous { get; set; } public System.DateTime? LastActivityDate { get; set; } public string Email { get; set; } public string PasswordQuestion { get; set; } public string PasswordAnswer { get; set; } public bool IsApproved { get; set; } public bool IsLockedOut { get; set; } public System.DateTime? CreateDate { get; set; } public System.DateTime? LastLoginDate { get; set; } public System.DateTime? LastPasswordChangedDate { get; set; } public System.DateTime? LastLockoutDate { get; set; } public int FailedPasswordAttemptCount { get; set; } public System.DateTime? FailedPasswordAttemptWindowStart { get; set; } public int FailedPasswordAnswerAttemptCount { get; set; } public System.DateTime? FailedPasswordAnswerAttemptWindowStart { get; set; } public string Comment { get; set; } public ProfileInfo Profile { get; set; } } }
انتقال داده پروفایلها به جداول جدید
آخرین نسخه پکیج Entity Framework را نصب کنید. همچنین یک رفرنس به اپلیکیشن وب پروژه بدهید (کلیک راست روی پروژه و گزینه 'Add Reference').
کد زیر را در کلاس Program.cs وارد کنید. این قطعه کد پروفایل تک تک کاربران را میخواند و در قالب 'ProfileInfo' آنها را serialize میکند و در دیتابیس ذخیره میکند.
public class Program { var dbContext = new ApplicationDbContext(); foreach (var profile in dbContext.Profiles) { var stringId = profile.UserId.ToString(); var user = dbContext.Users.Where(x => x.Id == stringId).FirstOrDefault(); Console.WriteLine("Adding Profile for user:" + user.UserName); var serializer = new XmlSerializer(typeof(ProfileInfo)); var stringReader = new StringReader(profile.PropertyValueStrings); var profileData = serializer.Deserialize(stringReader) as ProfileInfo; if (profileData == null) { Console.WriteLine("Profile data deserialization error for user:" + user.UserName); } else { user.Profile = profileData; } } dbContext.SaveChanges(); }
برخی از مدلهای استفاده شده در پوشه 'IdentityModels' تعریف شده اند که در پروژه اپلیکیشن وبمان قرار دارند، بنابراین افزودن فضاهای نام مورد نیاز فراموش نشود.
کد بالا روی دیتابیسی که در پوشه App_Data وجود دارد کار میکند، این دیتابیس در مراحل قبلی در اپلیکیشن وب پروژه ایجاد شد. برای اینکه این دیتابیس را رفرنس کنیم باید رشته اتصال فایل app.config اپلیکیشن کنسول را بروز رسانی کنید. از همان رشته اتصال web.config در اپلیکیشن وب پروژه استفاده کنید. همچنین آدرس فیزیکی کامل را در خاصیت 'AttachDbFilename' وارد کنید.
یک Command Prompt باز کنید و به پوشه bin اپلیکیشن کنسول بالا بروید. فایل اجرایی را اجرا کنید و نتیجه را مانند تصویر زیر بررسی کنید.
در پنجره Server Explorer جدول 'AspNetUsers' را باز کنید. حال ستونهای این جدول باید خواص کلاس مدل را منعکس کنند.
کارایی سیستم را تایید کنید
ASP.NET MVC #18
public class EfRole : EfGenericService<Role>, IRole { public EfRole(IUnitOfWork uow) : base(uow) { } public bool IsUserInRole(string username, string roleName) { using (var context = new PublishingContext()) { var user = context.Users.Where(x => x.Username.Equals(username, StringComparison.CurrentCultureIgnoreCase)).FirstOrDefault(); var roles = from ur in user.Rolls from r in context.Rolls where ur.Id == r.Id select r.Rol; if (user != null) return roles.Any(x => x.Equals(roleName, StringComparison.CurrentCultureIgnoreCase)); else return false; } } }
PM> Install-Package DNTFrameworkCore.Web
PM> Install-Package DNTFrameworkCore.Web.EntityFramework
public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build() .MigrateDbContext<ProjectDbContext>() .Run(); } //... }
[Route("api/[controller]")] public class BlogsController : CrudController<IBlogService, int, BlogModel> { public BlogsController(IBlogService service) : base(service) { } protected override string CreatePermissionName => PermissionNames.Blogs_Create; protected override string EditPermissionName => PermissionNames.Blogs_Edit; protected override string ViewPermissionName => PermissionNames.Blogs_View; protected override string DeletePermissionName => PermissionNames.Blogs_Delete; }
var data = JSON.parse(responseBody) pm.environment.set("token", data.token);
حال برای ارسال درخواستهای HTTP به BlogsController به شکل زیر عمل کنید:
query={"page":1,"pageSize":100,"filter":{"logic":"and","filters":[{"field":"title","value":"Blog1","operator":"startswith"}]}}
{{endpoint}}/blogs/10
[Route("api/[controller]")] public class UsersController : CrudController<IUserService, long, UserReadModel, UserModel> { private readonly ILookupService _lookupService; public UsersController(IUserService service, ILookupService lookupService) : base(service) { _lookupService = lookupService ?? throw new ArgumentNullException(nameof(lookupService)); } protected override string CreatePermissionName => PermissionNames.Users_Create; protected override string EditPermissionName => PermissionNames.Users_Edit; protected override string ViewPermissionName => PermissionNames.Users_View; protected override string DeletePermissionName => PermissionNames.Users_Delete; [HttpGet("[action]")] [PermissionAuthorize(PermissionNames.Users_Create, PermissionNames.Users_Edit)] public async Task<IActionResult> RoleList() { var result = await _lookupService.ReadRolesAsync(); return Ok(result); } }
باتوجه به اینکه UserRole به عنوان یکی از وابستگیهای موجودیت User محسوب میشود، در پاسخ درخواست GET مرتبط با کاربری با شناسه 2، roles آن، لیستی از UserRoleModel هستند که به عنوان یک DetailModel طراحی شده است. به عنوان مثال برای حذف اتصال یک گروه کاربری باید درخواست PUT را به شکل زیر ارسال کنید:
اینبار اگر برای درخواست GET کاربر با شناسه 2 اقدام کنیم، به خروجی زیر خواهم رسید:
{ "userName": "rabbal", "displayName": "غلامرضا ربال", "password": null, "isActive": false, "roles": [], "permissions": [], "ignoredPermissions": [], "rowVersion": "AAAAAAACGxI=", "id": 2 }
برای بررسی بیشتر، پیشنهاد میکنم پروژه DNTFrameworkCore.TestAPI موجود در مخزن این زیرساخت را بازبینی کنید.
تیم ASP.NET Identity پروژه نمونه ای را فراهم کرده است که میتواند بعنوان نقطه شروعی برای اپلیکیشنهای MVC استفاده شود. پیکربندیهای لازم در این پروژه انجام شدهاند و برای استفاده از فریم ورک جدید آماده است.
شروع به کار : پروژه نمونه را توسط NuGet ایجاد کنید
برای شروع یک پروژه ASP.NET خالی ایجاد کنید (در دیالوگ قالبها گزینه Empty را انتخاب کنید). سپس کنسول Package Manager را باز کرده و دستور زیر را اجرا کنید.
PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre
پس از اینکه NuGet کارش را به اتمام رساند باید پروژه ای با ساختار متداول پروژههای ASP.NET MVC داشته باشید. به تصویر زیر دقت کنید.
همانطور که میبینید ساختار پروژه بسیار مشابه پروژههای معمول MVC است، اما آیتمهای جدیدی نیز وجود دارند. فعلا تمرکز اصلی ما روی فایل IdentityConfig.cs است که در پوشه App_Start قرار دارد.
اگر فایل مذکور را باز کنید و کمی اسکرول کنید تعاریف دو کلاس سرویس را مشاهده میکنید: EmailService و SmsService.
public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Plug in your email service here to send an email. return Task.FromResult(0); } } public class SmsService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Plug in your sms service here to send a text message. return Task.FromResult(0); } }
اگر دقت کنید هر دو کلاس قرارداد IIdentityMessageService را پیاده سازی میکنند. میتوانید از این قرارداد برای پیاده سازی سرویسهای اطلاع رسانی ایمیلی، پیامکی و غیره استفاده کنید. در ادامه خواهیم دید چگونه این دو سرویس را بسط دهیم.
یک حساب کاربری مدیریتی پیش فرض ایجاد کنید
پیش از آنکه بیشتر جلو رویم نیاز به یک حساب کاربری در نقش مدیریتی داریم تا با اجرای اولیه اپلیکیشن در دسترس باشد. کلاسی بنام ApplicationDbInitializer در همین فایل وجود دارد که هنگام اجرای اولیه و یا تشخیص تغییرات در مدل دیتابیس، اطلاعاتی را Seed میکند.
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext> { protected override void Seed(ApplicationDbContext context) { InitializeIdentityForEF(context); base.Seed(context); } //Create User=Admin@Admin.com with password=Admin@123456 in the Admin role public static void InitializeIdentityForEF(ApplicationDbContext db) { var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>(); var roleManager = HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>(); const string name = "admin@admin.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 IdentityRole(roleName); var roleresult = roleManager.Create(role); } var user = userManager.FindByName(name); if (user == null) { user = new ApplicationUser { UserName = name, Email = name }; var result = userManager.Create(user, password); result = userManager.SetLockoutEnabled(user.Id, false); } // Add user admin to Role Admin if not already added var rolesForUser = userManager.GetRoles(user.Id); if (!rolesForUser.Contains(role.Name)) { var result = userManager.AddToRole(user.Id, role.Name); } } }
تایید حسابهای کاربری : چگونه کار میکند
بدون شک با تایید حسابهای کاربری توسط ایمیل آشنا هستید. حساب کاربری ای ایجاد میکنید و ایمیلی به آدرس شما ارسال میشود که حاوی لینک فعالسازی است. با کلیک کردن این لینک حساب کاربری شما تایید شده و میتوانید به سایت وارد شوید.
اگر به کنترلر AccountController در این پروژه نمونه مراجعه کنید متد Register را مانند لیست زیر مییابید.
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Register(RegisterViewModel model) { if (ModelState.IsValid) { var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; var result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id); var callbackUrl = Url.Action( "ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme); await UserManager.SendEmailAsync( user.Id, "Confirm your account", "Please confirm your account by clicking this link: <a href=\"" + callbackUrl + "\">link</a>"); ViewBag.Link = callbackUrl; return View("DisplayEmail"); } AddErrors(result); } // If we got this far, something failed, redisplay form return View(model); }
public static ApplicationUserManager Create( IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) { var manager = new ApplicationUserManager( new UserStore<ApplicationUser>( context.Get<ApplicationDbContext>())); // Configure validation logic for usernames manager.UserValidator = new UserValidator<ApplicationUser>(manager) { AllowOnlyAlphanumericUserNames = false, RequireUniqueEmail = true }; // Configure validation logic for passwords manager.PasswordValidator = new PasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = true, RequireDigit = true, RequireLowercase = true, RequireUppercase = true, }; // Configure user lockout defaults manager.UserLockoutEnabledByDefault = true; manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5); manager.MaxFailedAccessAttemptsBeforeLockout = 5; // Register two factor authentication providers. This application // uses Phone and Emails as a step of receiving a code for verifying the user // You can write your own provider and plug in here. manager.RegisterTwoFactorProvider( "PhoneCode", new PhoneNumberTokenProvider<ApplicationUser> { MessageFormat = "Your security code is: {0}" }); manager.RegisterTwoFactorProvider( "EmailCode", new EmailTokenProvider<ApplicationUser> { Subject = "SecurityCode", BodyFormat = "Your security code is {0}" }); manager.EmailService = new EmailService(); manager.SmsService = new SmsService(); var dataProtectionProvider = options.DataProtectionProvider; if (dataProtectionProvider != null) { manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>( dataProtectionProvider.Create("ASP.NET Identity")); } return manager; }
در قطعه کد بالا کلاسهای EmailService و SmsService روی وهله ApplicationUserManager تنظیم میشوند.
manager.EmailService = new EmailService(); manager.SmsService = new SmsService();
درست در بالای این کدها میبینید که چگونه تامین کنندگان احراز هویت دو مرحله ای (مبتنی بر ایمیل و پیامک) رجیستر میشوند.
// Register two factor authentication providers. This application // uses Phone and Emails as a step of receiving a code for verifying the user // You can write your own provider and plug in here. manager.RegisterTwoFactorProvider( "PhoneCode", new PhoneNumberTokenProvider<ApplicationUser> { MessageFormat = "Your security code is: {0}" }); manager.RegisterTwoFactorProvider( "EmailCode", new EmailTokenProvider<ApplicationUser> { Subject = "SecurityCode", BodyFormat = "Your security code is {0}" });
تایید حسابهای کاربری توسط ایمیل و احراز هویت دو مرحله ای توسط ایمیل و/یا پیامک نیاز به پیاده سازی هایی معتبر از قراردارد IIdentityMessageService دارند.
پیاده سازی سرویس ایمیل توسط ایمیل خودتان
پیاده سازی سرویس ایمیل نسبتا کار ساده ای است. برای ارسال ایمیلها میتوانید از اکانت ایمیل خود و یا سرویس هایی مانند SendGrid استفاده کنید. بعنوان مثال اگر بخواهیم سرویس ایمیل را طوری پیکربندی کنیم که از یک حساب کاربری Outlook استفاده کند، مانند زیر عمل خواهیم کرد.
public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Credentials: var credentialUserName = "yourAccount@outlook.com"; var sentFrom = "yourAccount@outlook.com"; var pwd = "yourApssword"; // Configure the client: System.Net.Mail.SmtpClient client = new System.Net.Mail.SmtpClient("smtp-mail.outlook.com"); client.Port = 587; client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network; client.UseDefaultCredentials = false; // Creatte the credentials: System.Net.NetworkCredential credentials = new System.Net.NetworkCredential(credentialUserName, pwd); client.EnableSsl = true; client.Credentials = credentials; // Create the message: var mail = new System.Net.Mail.MailMessage(sentFrom, message.Destination); mail.Subject = message.Subject; mail.Body = message.Body; // Send: return client.SendMailAsync(mail); } }
پیاده سازی سرویس ایمیل با استفاده از SendGrid
سرویسهای ایمیل متعددی وجود دارند اما یکی از گزینههای محبوب در جامعه دات نت SendGrid است. این سرویس API قدرتمندی برای زبانهای برنامه نویسی مختلف فراهم کرده است. همچنین یک Web API مبتنی بر HTTP نیز در دسترس است. قابلیت دیگر اینکه این سرویس مستقیما با Windows Azure یکپارچه میشود.
می توانید در سایت SendGrid یک حساب کاربری رایگان بعنوان توسعه دهنده بسازید. پس از آن پیکربندی سرویس ایمیل با مرحله قبل تفاوت چندانی نخواهد داشت. پس از ایجاد حساب کاربری توسط تیم پشتیبانی SendGrid با شما تماس گرفته خواهد شد تا از صحت اطلاعات شما اطمینان حاصل شود. برای اینکار چند گزینه در اختیار دارید که بهترین آنها ایجاد یک اکانت ایمیل در دامنه وب سایتتان است. مثلا اگر هنگام ثبت نام آدرس وب سایت خود را www.yourwebsite.com وارد کرده باشید، باید ایمیلی مانند info@yourwebsite.com ایجاد کنید و توسط ایمیل فعالسازی آن را تایید کند تا تیم پشتیبانی مطمئن شود صاحب امتیاز این دامنه خودتان هستید.
تنها چیزی که در قطعه کد بالا باید تغییر کند اطلاعات حساب کاربری و تنظیمات SMTP است. توجه داشته باشید که نام کاربری و آدرس فرستنده در اینجا متفاوت هستند. در واقع میتوانید از هر آدرسی بعنوان آدرس فرستنده استفاده کنید.
public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Credentials: var sendGridUserName = "yourSendGridUserName"; var sentFrom = "whateverEmailAdressYouWant"; var sendGridPassword = "YourSendGridPassword"; // Configure the client: var client = new System.Net.Mail.SmtpClient("smtp.sendgrid.net", Convert.ToInt32(587)); client.Port = 587; client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network; client.UseDefaultCredentials = false; // Creatte the credentials: System.Net.NetworkCredential credentials = new System.Net.NetworkCredential(credentialUserName, pwd); client.EnableSsl = true; client.Credentials = credentials; // Create the message: var mail = new System.Net.Mail.MailMessage(sentFrom, message.Destination); mail.Subject = message.Subject; mail.Body = message.Body; // Send: return client.SendMailAsync(mail); } }
آزمایش تایید حسابهای کاربری توسط سرویس ایمیل
ابتدا اپلیکیشن را اجرا کنید و سعی کنید یک حساب کاربری جدید ثبت کنید. دقت کنید که از آدرس ایمیلی زنده که به آن دسترسی دارید استفاده کنید. اگر همه چیز بدرستی کار کند باید به صفحه ای مانند تصویر زیر هدایت شوید.
همانطور که مشاهده میکنید پاراگرافی در این صفحه وجود دارد که شامل لینک فعالسازی است. این لینک صرفا جهت تسهیل کار توسعه دهندگان درج میشود و هنگام توزیع اپلیکیشن باید آن را حذف کنید. در ادامه به این قسمت باز میگردیم. در این مرحله ایمیلی حاوی لینک فعالسازی باید برای شما ارسال شده باشد.
پیاده سازی سرویس SMS
برای استفاده از احراز هویت دو مرحله ای پیامکی نیاز به یک فراهم کننده SMS دارید، مانند Twilio . مانند SendGrid این سرویس نیز در جامعه دات نت بسیار محبوب است و یک C# API قدرتمند ارائه میکند. میتوانید حساب کاربری رایگانی بسازید و شروع به کار کنید.
پس از ایجاد حساب کاربری یک شماره SMS، یک شناسه SID و یک شناسه Auth Token به شما داده میشود. شماره پیامکی خود را میتوانید پس از ورود به سایت و پیمایش به صفحه Numbers مشاهده کنید.
شناسههای SID و Auth Token نیز در صفحه Dashboard قابل مشاهده هستند.
اگر دقت کنید کنار شناسه Auth Token یک آیکون قفل وجود دارد که با کلیک کردن روی آن شناسه مورد نظر نمایان میشود.
حال میتوانید از سرویس Twilio در اپلیکیشن خود استفاده کنید. ابتدا بسته NuGet مورد نیاز را نصب کنید.
PM> Install-Package Twilio
public class SmsService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { string AccountSid = "YourTwilioAccountSID"; string AuthToken = "YourTwilioAuthToken"; string twilioPhoneNumber = "YourTwilioPhoneNumber"; var twilio = new TwilioRestClient(AccountSid, AuthToken); twilio.SendSmsMessage(twilioPhoneNumber, message.Destination, message.Body); // Twilio does not return an async Task, so we need this: return Task.FromResult(0); } }
حال که سرویسهای ایمیل و پیامک را در اختیار داریم میتوانیم احراز هویت دو مرحله ای را تست کنیم.
آزمایش احراز هویت دو مرحله ای
پروژه نمونه جاری طوری پیکربندی شده است که احراز هویت دو مرحله ای اختیاری است و در صورت لزوم میتواند برای هر کاربر بصورت جداگانه فعال شود. ابتدا توسط حساب کاربری مدیر، یا حساب کاربری ای که در قسمت تست تایید حساب کاربری ایجاد کرده اید وارد سایت شوید. سپس در سمت راست بالای صفحه روی نام کاربری خود کلیک کنید. باید صفحه ای مانند تصویر زیر را مشاهده کنید.
در این قسمت باید احراز هویت دو مرحله ای را فعال کنید و شماره تلفن خود را ثبت نمایید. پس از آن یک پیام SMS برای شما ارسال خواهد شد که توسط آن میتوانید پروسه را تایید کنید. اگر همه چیز بدرستی کار کند این مراحل چند ثانیه بیشتر نباید زمان بگیرد، اما اگر مثلا بیش از 30 ثانیه زمان برد احتمالا اشکالی در کار است.
حال که احراز هویت دو مرحله ای فعال شده از سایت خارج شوید و مجددا سعی کنید به سایت وارد شوید. در این مرحله یک انتخاب به شما داده میشود. میتوانید کد احراز هویت دو مرحله ای خود را توسط ایمیل یا پیامک دریافت کنید.
پس از اینکه گزینه خود را انتخاب کردید، کد احراز هویت دو مرحله ای برای شما ارسال میشود که توسط آن میتوانید پروسه ورود به سایت را تکمیل کنید.
حذف میانبرهای آزمایشی
همانطور که گفته شد پروژه نمونه شامل میانبرهایی برای تسهیل کار توسعه دهندگان است. در واقع اصلا نیازی به پیاده سازی سرویسهای ایمیل و پیامک ندارید و میتوانید با استفاده از این میانبرها حسابهای کاربری را تایید کنید و کدهای احراز هویت دو مرحله ای را نیز مشاهده کنید. اما قطعا این میانبرها پیش از توزیع اپلیکیشن باید حذف شوند.
بدین منظور باید نماها و کدهای مربوطه را ویرایش کنیم تا اینگونه اطلاعات به کلاینت ارسال نشوند. اگر کنترلر AccountController را باز کنید و به متد ()Register بروید با کد زیر مواجه خواهید شد.
if (result.Succeeded) { var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id); var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme); await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking this link: <a href=\"" + callbackUrl + "\">link</a>"); // This should not be deployed in production: ViewBag.Link = callbackUrl; return View("DisplayEmail"); } AddErrors(result);
نمایی که این متد باز میگرداند یعنی DisplayEmail.cshtml نیز باید ویرایش شود.
@{ ViewBag.Title = "DEMO purpose Email Link"; } <h2>@ViewBag.Title.</h2> <p class="text-info"> Please check your email and confirm your email address. </p> <p class="text-danger"> For DEMO only: You can click this link to confirm the email: <a href="@ViewBag.Link">link</a> Please change this code to register an email service in IdentityConfig to send an email. </p>
متد دیگری که در این کنترلر باید ویرایش شود ()VerifyCode است که کد احراز هویت دو مرحله ای را به صفحه مربوطه پاس میدهد.
[AllowAnonymous] public async Task<ActionResult> VerifyCode(string provider, string returnUrl) { // Require that the user has already logged in via username/password or external login if (!await SignInHelper.HasBeenVerified()) { return View("Error"); } var user = await UserManager.FindByIdAsync(await SignInHelper.GetVerifiedUserIdAsync()); if (user != null) { ViewBag.Status = "For DEMO purposes the current " + provider + " code is: " + await UserManager.GenerateTwoFactorTokenAsync(user.Id, provider); } return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl }); }
همانطور که میبینید متغیری بنام Status به ViewBag اضافه میشود که باید حذف شود.
نمای این متد یعنی VerifyCode.cshtml نیز باید ویرایش شود.
@model IdentitySample.Models.VerifyCodeViewModel @{ ViewBag.Title = "Enter Verification Code"; } <h2>@ViewBag.Title.</h2> @using (Html.BeginForm("VerifyCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { @Html.AntiForgeryToken() @Html.ValidationSummary("", new { @class = "text-danger" }) @Html.Hidden("provider", @Model.Provider) <h4>@ViewBag.Status</h4> <hr /> <div class="form-group"> @Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" }) <div class="col-md-10"> @Html.TextBoxFor(m => m.Code, new { @class = "form-control" }) </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <div class="checkbox"> @Html.CheckBoxFor(m => m.RememberBrowser) @Html.LabelFor(m => m.RememberBrowser) </div> </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" class="btn btn-default" value="Submit" /> </div> </div> }
در این فایل کافی است ViewBag.Status را حذف کنید.
از تنظیمات ایمیل و SMS محافظت کنید
در مثال جاری اطلاعاتی مانند نام کاربری و کلمه عبور، شناسههای SID و Auth Token همگی در کد برنامه نوشته شده اند. بهتر است چنین مقادیری را بیرون از کد اپلیکیشن نگاه دارید، مخصوصا هنگامی که پروژه را به سرویس کنترل ارسال میکند (مثلا مخازن عمومی مثل GitHub). بدین منظور میتوانید یکی از پستهای اخیر را مطالعه کنید.
EF Code First #12
من چند روز پیش مطلبی رو راجع به ساخت attribute برای compare validation در روش code first عنوان کردم(البته در asp.net web form).که در واقع اصلیترین کاربردش همون کاربرد معروف مقایسه رمز عبور و تکرار رمز عبور هست. یه سری کارایی در این زمینه انجام دادم و خواستم در مورد مشکل مربوطه و در کل ، صحیح یا غلط بودن این روش کمکم کنید.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] public class CompareAttribute : ValidationAttribute { public CompareAttribute(string originalProperty, string confirmProperty) { OriginalProperty = originalProperty; ConfirmProperty = confirmProperty; } public string ConfirmProperty { get; private set; } public string OriginalProperty { get; private set; } public override bool IsValid(object value) { PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value); return String.Equals(((User)value).Password, ((User)value).RepeatPassword); } }
//[CompareAttribute("Password", "RepeatPassword", ErrorMessage = "Not Compare!")] public class User { public Int64 UserId { get; set; }
[Required(ErrorMessageResourceName = "Password", ErrorMessageResourceType = typeof(ErrorMessageResource))] public String Password { get; set; }
[NotMapped] [Required(ErrorMessageResourceName = "RepeatPassword", ErrorMessageResourceType = typeof(ErrorMessageResource))] //[CompareAttribute("Password","RepeatPassword" , ErrorMessage = "Not Compare!")] public String RepeatPassword { get; set; } }
وقتی اونو بالای کلاس User میزارم کار میکنه.ولی خب این یه کار خیلی زشتیه! چون به ازای همه propertyها اجرا میشه.ولی وقتی اونو تو جای اصلیش که همون بالای repeat password هست میزارم کار نمیکنه.
یه جورایی مشکل از اینه که نمیشه انگار مقدار property رو به این سمت پاس داد.ولی در حالتی که بالای کلاس میزارمش چون خود کلاس رو پاس میده ، طبیعتاٌ
اسم propertyها رو هم پیدا میکنه.
ممنون میشم راهنماییم کنید .
تعریف نیازمندیهای اپلیکیشن
- تنها کاربران احراز هویت شده قادر خواهند بود تا لیست ToDoهای خود را ببینند، آیتمهای جدید ثبت کنند یا دادههای قبلی را ویرایش و حذف کنند.
- کاربران نباید آیتمهای ایجاد شده توسط دیگر کاربران را ببینند.
- تنها کاربرانی که به نقش Admin تعلق دارند باید بتوانند تمام ToDoهای ایجاد شده را ببینند.
Database.SetInitializer<MyDbContext>(new MyDbInitializer());
ایجاد نقش مدیر و کاربر جدیدی که به این نقش تعلق دارد
public class MyDbInitializer : DropCreateDatabaseAlways<MyDbContext> { protected override void Seed(MyDbContext context) { var UserManager = new UserManager<MyUser>(new UserStore<MyUser>(context)); var RoleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(context)); string name = "Admin"; string password = "123456"; //Create Role Admin if it does not exist if (!RoleManager.RoleExists(name)) { var roleresult = RoleManager.Create(new IdentityRole(name)); } //Create User=Admin with password=123456 var user = new MyUser(); user.UserName = name; var adminresult = UserManager.Create(user, password); //Add User Admin to Role Admin if (adminresult.Succeeded) { var result = UserManager.AddToRole(user.Id, name); } base.Seed(context); } }
public class MyUser : IdentityUser { public string HomeTown { get; set; } public virtual ICollection<ToDo> ToDoes { get; set; } } public class ToDo { public int Id { get; set; } public string Description { get; set; } public bool IsDone { get; set; } public virtual MyUser User { get; set; } }
public class MyDbContext : IdentityDbContext<MyUser> { public MyDbContext() : base("DefaultConnection") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { public System.Data.Entity.DbSet<AspnetIdentitySample.Models.ToDo> ToDoes { get; set; } }
تنها کاربران احراز هویت شده باید قادر به اجرای عملیات CRUD باشند
[Authorize] public class ToDoController : Controller
private MyDbContext db; private UserManager<MyUser> manager; public ToDoController() { db = new MyDbContext(); manager = new UserManager<MyUser>(new UserStore<MyUser>(db)); }
اکشن متد Create را بروز رسانی کنید
public async Task<ActionResult> Create ([Bind(Include="Id,Description,IsDone")] ToDo todo) { var currentUser = await manager.FindByIdAsync (User.Identity.GetUserId()); if (ModelState.IsValid) { todo.User = currentUser; db.ToDoes.Add(todo); await db.SaveChangesAsync(); return RedirectToAction("Index"); } return View(todo); }
اکشن متد List را بروز رسانی کنید
public ActionResult Index() { var currentUser = manager.FindById(User.Identity.GetUserId()); return View(db.ToDoes.ToList().Where( todo => todo.User.Id == currentUser.Id)); }
تنها مدیران سایت باید بتوانند تمام ToDoها را ببینند
[Authorize(Roles="Admin")] public async Task<ActionResult> All() { return View(await db.ToDoes.ToListAsync()); }
نمایش جزئیات کاربران از جدول ToDo ها
@model IEnumerable<AspnetIdentitySample.Models.ToDo> @{ ViewBag.Title = "Index"; } <h2>List of ToDoes for all Users</h2> <p> Notice that we can see the User info (UserName) and profile info such as HomeTown for the user as well. This was possible because we associated the User object with a ToDo object and hence we can get this rich behavior. 12: </p> <table class="table"> <tr> <th> @Html.DisplayNameFor(model => model.Description) </th> <th> @Html.DisplayNameFor(model => model.IsDone) </th> <th>@Html.DisplayNameFor(model => model.User.UserName)</th> <th>@Html.DisplayNameFor(model => model.User.HomeTown)</th> </tr> 25: 26: @foreach (var item in Model) 27: { 28: <tr> 29: <td> 30: @Html.DisplayFor(modelItem => item.Description) 31: </td> 32: <td> @Html.DisplayFor(modelItem => item.IsDone) </td> <td> @Html.DisplayFor(modelItem => item.User.UserName) </td> <td> @Html.DisplayFor(modelItem => item.User.HomeTown) </td> </tr> } </table>
صفحه Layout را بروز رسانی کنید تا به ToDoها لینک شود
<li>@Html.ActionLink("ToDo", "Index", "ToDo")</li> <li>@Html.ActionLink("ToDo for User In Role Admin", "All", "ToDo")</li>
ساخت یک ToDo بعنوان کاربر عادی
پس از ساختن یک ToDo میتوانید لیست رکوردهای خود را مشاهده کنید. دقت داشته باشید که رکوردهایی که کاربران دیگر ثبت کرده اند برای شما نمایش داده نخواهند شد.
مشاهده تمام ToDoها بعنوان مدیر سایت
User = Admin Password = 123456
یکپارچه سازی اعتبارسنجی EF Code first با امکانات WPF و حذف کدهای تکرای INotifyPropertyChanged
بررسی سطح بالای مکانیزمهای اعتبارسنجی و AOP بکارگرفته شده
در حین کار با قالب پروژه WPF Framework، هنگام طراحی Modelهای خود (تفاوتی نمیکند که Domain model باشند یا صرفا Model متناظر با یک View)، نیاز است دو مورد را رعایت کنید:
[ImplementPropertyChanged] // AOP public class LoginPageModel : DataErrorInfoBase
ب) از کلاس پایه DataErrorInfoBase مشتق گردد
البته اگر به کلاسهای Domain model برنامه مراجعه کنید، صرفا مشتق شدن از BaseEntity را ملاحظه میکنید:
public class User : BaseEntity
[ImplementPropertyChanged] // AOP public abstract class BaseEntity : DataErrorInfoBase //پیاده سازی خودکار سیستم اعتبارسنجی یکپارچه
بررسی جزئیات مکانیزم AOP بکارگرفته شده
بسیار خوب؛ اینها چطور کار میکنند؟!
ابتدا نیاز است مطلب «معرفی پروژه NotifyPropertyWeaver» را یکبار مطالعه نمائید. خلاصهای جهت تکرار نکات مهم آن:
ویژگی ImplementPropertyChanged به ابزار Fody اعلام میکند که لطفا کدهای تکراری INotifyPropertyChanged را پس از کامپایل اسمبلی جاری، بر اساس تزریق کدهای IL متناظر، به اسمبلی اضافه کن. این روش از لحاظ کارآیی و همچنین تمیز نگه داشتن کدهای نهایی برنامه، فوق العاده است.
برای بررسی کارکرد آن نیاز است اسمبلی مثلا Models را دیکامپایل کرد:
همانطور که ملاحظه میکنید، کدهای تکراری INotifyPropertyChanged به صورت خودکار به اسمبلی نهایی اضافه شدهاند.
البته بدیهی است که استفاده از Fody الزامی نیست. اگر علاقمند هستید که این اطلاعات را دستی اضافه کنید، بهتر است از کلاس پایه BaseViewModel قرار گرفته در مسیر MVVM\BaseViewModel.cs پروژه Common استفاده نمائید.
در این کلاس، پیاده سازیهای NotifyPropertyChanged را بر اساس متدهایی که یک رشته را به عنوان نام خاصیت دریافت میکنند و یا متدی که امکان دسترسی strongly typed به نام رشته را میسر ساخته است، ملاحظه میکنید.
/// <summary> /// تغییر مقدار یک خاصیت را اطلاع رسانی خواهد کرد /// </summary> /// <param name="propertyName">نام خاصیت</param> public void NotifyPropertyChanged(string propertyName) /// <summary> /// تغییر مقدار یک خاصیت را اطلاع رسانی خواهد کرد /// </summary> /// <param name="expression">نام خاصیت مورد نظر</param> public void NotifyPropertyChanged(Expression<Func<object>> expression)
public class AlertConfirmBoxViewModel : BaseViewModel { AlertConfirmBoxModel _alertConfirmBoxModel; public AlertConfirmBoxModel AlertConfirmBoxModel { set { _alertConfirmBoxModel = value; NotifyPropertyChanged("AlertConfirmBoxModel"); // ویا .... NotifyPropertyChanged(()=>AlertConfirmBoxModel); } get { return _alertConfirmBoxModel; } }
بررسی جزئیات اعتبارسنجیهای تعریف شده
EF دارای یک سری ویژگی مانند Required و امثال آن است. WPF دارای اینترفیسی است به نام IDataErrorInfo. این دو را باید به نحوی به هم مرتبط ساخت که پیاده سازیهای مرتبط با آنها را در مسیرهای WpfValidation\DataErrorInfoBase.cs و WpfValidation\ValidationHelper.cs پروژه Common میتوانید ملاحظه نمائید.
<TextBox Text="{Binding Path=ChangeProfileData.UserName, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, NotifyOnValidationError=true, ValidatesOnExceptions=true, ValidatesOnDataErrors=True, TargetNullValue=''}" />
همچنین حالتهای بررسی اعتبارسنجی آن نیز به PropertyChanged تنظیم گردیده است. در این حالت WPF به تعاریف شیء ChangeProfileData مراجعه کرده و برای نمونه اگر این شیء اینترفیس IDataErrorInfo را پیاده سازی کرده بود، نام خاصیت جاری را به آن ارسال و از آن خطاهای اعتبارسنجی متناظر را درخواست میکند. در اینجا وقت خواهیم داشت تا بر اساس ویژگیها و Data annotaions اعمالی، کار اعتبارسنجی را انجام داده و نتیجه را بازگشت دهیم.
خلاصهی تمام این اعمال و کلاسها، در کلاس پایه DataErrorInfoBase این قالب پروژه قرار گرفتهاند. بنابراین تنها کاری که باید صورت گیرد، مشتق کردن کلاس مدل مورد نظر از آن میباشد.
همچنین باید دقت داشت که نمایش اطلاعات خطاهای حاصل از اعتبارسنجی در این قالب پروژه بر اساس امکانات قالب متروی MahApps.Metro انجام میگیرد (این مورد از Silverlight toolkit به ارث رسیده است) و در حالت کلی خودکار نیست؛ اما در اینجا نیازی به کدنویسی اضافهتری ندارد.
به علاوه باید دقت داشت که این مورد ویژه را باید بر اساس آخرین Build کتابخانه MahApps.Metro که بهروزتر است دریافت و استفاده کرد. در اینجا با پارامتر Pre ذکر شده است.
PM> Install-Package MahApps.Metro -Pre
public class User { public int Id { get; set; } [Unique] public string Email { get; set; } [Unique("MyUniqueIndex",UniqueIndexOrder.ASC)] public string Username { get; set; } [Unique(UniqueIndexOrder.DESC)] public string PersonalCode{ get; set; } public string Password { get; set; } public string FirstName { get; set; } public string LastName { get; set; } }
using System; namespace SampleUniqueIndex { [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public class UniqueAttribute : Attribute { public UniqueAttribute(UniqueIndexOrder order = UniqueIndexOrder.ASC) { Order = order; } public UniqueAttribute(string indexName,UniqueIndexOrder order = UniqueIndexOrder.ASC) { IndexName = indexName; Order = order; } public string IndexName { get; private set; } public UniqueIndexOrder Order { get; set; } } public enum UniqueIndexOrder { ASC, DESC } }
using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Data.Metadata.Edm; using System.Linq; using System.Reflection; namespace SampleUniqueIndex { public static class DbContextExtention { private static BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy; public static void ExecuteUniqueIndexes(this DbContext context) { var tables = GetTables(context); var query = ""; foreach (var dbSet in GetDbSets(context)) { var entityType = dbSet.PropertyType.GetGenericArguments().First(); var table = tables[entityType.Name]; var currentIndexes = GetCurrentUniqueIndexes(context, table.TableName); foreach (var uniqueProp in GetUniqueProperties(context, entityType, table)) { var indexName = string.IsNullOrWhiteSpace(uniqueProp.IndexName) ? "IX_Unique_" + uniqueProp.TableName + "_" + uniqueProp.FieldName : uniqueProp.IndexName; if (!currentIndexes.Contains(indexName)) { query += "ALTER TABLE [" + table.TableSchema + "].[" + table.TableName + "] ADD CONSTRAINT [" + indexName + "] UNIQUE ([" + uniqueProp.FieldName + "] " + uniqueProp.Order + "); "; } else { currentIndexes.Remove(indexName); } } foreach (var index in currentIndexes) { query += "ALTER TABLE [" + table.TableSchema + "].[" + table.TableName + "] DROP CONSTRAINT " + index + "; "; } } if (query.Length > 0) context.Database.ExecuteSqlCommand(query); } private static List<string> GetCurrentUniqueIndexes(DbContext context, string tableName) { var sql = "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS where table_name = '" + tableName + "' and CONSTRAINT_TYPE = 'UNIQUE'"; var result = context.Database.SqlQuery<string>(sql).ToList(); return result; } private static IEnumerable<PropertyDescriptor> GetDbSets(DbContext context) { foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(context)) { var notMapped = prop.GetType().GetCustomAttributes(typeof(NotMappedAttribute),true); if (prop.PropertyType.Name == typeof(DbSet<>).Name && notMapped.Length == 0) yield return prop; } } private static List<UniqueProperty> GetUniqueProperties(DbContext context, Type entity, TableInfo tableInfo) { var indexedProperties = new List<UniqueProperty>(); var properties = entity.GetProperties(PublicInstance); var tableName = tableInfo.TableName; foreach (var prop in properties) { if (!prop.PropertyType.IsValueType && prop.PropertyType != typeof(string)) continue; UniqueAttribute[] uniqueAttributes = (UniqueAttribute[])prop.GetCustomAttributes(typeof(UniqueAttribute), true); NotMappedAttribute[] notMappedAttributes = (NotMappedAttribute[])prop.GetCustomAttributes(typeof(NotMappedAttribute), true); if (uniqueAttributes.Length > 0 && notMappedAttributes.Length == 0) { var fieldName = GetFieldName(context, entity, prop.Name); if (fieldName != null) { indexedProperties.Add(new UniqueProperty { TableName = tableName, IndexName = uniqueAttributes[0].IndexName, FieldName = fieldName, Order = uniqueAttributes[0].Order.ToString() }); } } } return indexedProperties; } private static Dictionary<string, TableInfo> GetTables(DbContext context) { var tablesInfo = new Dictionary<string, TableInfo>(); var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace; var tables = metadata.GetItemCollection(DataSpace.SSpace) .GetItems<EntityContainer>() .Single() .BaseEntitySets .OfType<EntitySet>() .Where(s => !s.MetadataProperties.Contains("Type") || s.MetadataProperties["Type"].ToString() == "Tables"); foreach (var table in tables) { var tableName = table.MetadataProperties.Contains("Table") && table.MetadataProperties["Table"].Value != null ? table.MetadataProperties["Table"].Value.ToString() : table.Name; var tableSchema = table.MetadataProperties["Schema"].Value.ToString(); tablesInfo.Add(table.Name, new TableInfo { EntityName = table.Name, TableName = tableName, TableSchema = tableSchema, }); } return tablesInfo; } public static string GetFieldName(DbContext context, Type entityModel, string propertyName) { var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace; var osMembers = metadata.GetItem<EntityType>(entityModel.FullName, DataSpace.OSpace).Properties; var ssMebers = metadata.GetItem<EntityType>("CodeFirstDatabaseSchema." + entityModel.Name, DataSpace.SSpace).Properties; if (!osMembers.Contains(propertyName)) return null; var index = osMembers.IndexOf(osMembers[propertyName]); return ssMebers[index].Name; } internal class UniqueProperty { public string TableName { get; set; } public string FieldName { get; set; } public string IndexName { get; set; } public string Order { get; set; } } internal class TableInfo { public string EntityName { get; set; } public string TableName { get; set; } public string TableSchema { get; set; } } } }
public static void ExecuteUniqueIndexes(this DbContext context) { var tables = GetTables(context); ... }
private static Dictionary<string, TableInfo> GetTables(DbContext context) { var tablesInfo = new Dictionary<string, TableInfo>(); var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace; var tables = metadata.GetItemCollection(DataSpace.SSpace) .GetItems<EntityContainer>() .Single() .BaseEntitySets .OfType<EntitySet>() .Where(s => !s.MetadataProperties.Contains("Type") || s.MetadataProperties["Type"].ToString() == "Tables"); foreach (var table in tables) { var tableName = table.MetadataProperties.Contains("Table") && table.MetadataProperties["Table"].Value != null ? table.MetadataProperties["Table"].Value.ToString() : table.Name; var tableSchema = table.MetadataProperties["Schema"].Value.ToString(); tablesInfo.Add(table.Name, new TableInfo { EntityName = table.Name, TableName = tableName, TableSchema = tableSchema, }); } return tablesInfo; }
public static void ExecuteUniqueIndexes(this DbContext context) { var tables = GetTables(context); var query = ""; foreach (var dbSet in GetDbSets(context)) { .... }
public static void ExecuteUniqueIndexes(this DbContext context) { ... foreach (var dbSet in GetDbSets(context)) { var entityType = dbSet.PropertyType.GetGenericArguments().First(); var table = tables[entityType.Name]; var currentIndexes = GetCurrentUniqueIndexes(context, table.TableName); } }
public static string GetFieldName(DbContext context, Type entityModel, string propertyName) { var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace; var osMembers = metadata.GetItem<EntityType>(entityModel.FullName, DataSpace.OSpace).Properties; var ssMebers = metadata.GetItem<EntityType>("CodeFirstDatabaseSchema." + entityModel.Name, DataSpace.SSpace).Properties; if (!osMembers.Contains(propertyName)) return null; var index = osMembers.IndexOf(osMembers[propertyName]); return ssMebers[index].Name; }
protected override void Seed(myDbContext context) { // This method will be called after migrating to the latest version. // You can use the DbSet<T>.AddOrUpdate() helper extension method // to avoid creating duplicate seed data. E.g. // // context.People.AddOrUpdate( // p => p.FullName, // new Person { FullName = "Andrew Peters" }, // new Person { FullName = "Brice Lambson" }, // new Person { FullName = "Rowan Miller" } // ); // context.ExecuteUniqueIndexes(); }
public class User { public string UserName { get; set; } public bool IsRole { get; set; } public HashSet<string> ConnectionIds { get; set; } } [Authorize] [HubName("userActivityHub")] public class UserActivityHub : Hub { private static readonly ConcurrentDictionary<string, User> Users = new ConcurrentDictionary<string, User>(); public void AdminJoin() { Groups.Add(Context.ConnectionId, "admins"); } public void Join() { var userName = Context.User.Identity.Name; var connectionId = Context.ConnectionId; var isAdmin = Context.User.IsInRole("Admin"); var user = Users.GetOrAdd(userName, _ => new User { UserName = userName, IsRole = isAdmin, ConnectionIds = new HashSet<string>() }); if (user.IsRole == true) { Groups.Add(user.ConnectionIds.ToString(), "admins"); } else { lock (user.ConnectionIds) { user.ConnectionIds.Add(connectionId); } Clients.Group("admins").showUserCount(Users.Count(a => a.Value.IsRole != true)); } } public void GetUserCount() { Clients.Group("admins").showUserCount(Users.Count(a => a.Value.IsRole != true)); } public override System.Threading.Tasks.Task OnDisconnected(bool stopCalled) { if (stopCalled) { var userName = Context.User.Identity.Name; var connectionId = Context.ConnectionId; User user; Users.TryGetValue(userName, out user); if (user != null) { lock (user.ConnectionIds) { user.ConnectionIds.RemoveWhere(cid => cid.Equals(connectionId)); if (!user.ConnectionIds.Any()) { User removeUser; Users.TryRemove(userName, out removeUser); } } } return Clients.Group("admins").showUserCount(Users.Count(a => a.Value.IsRole != true)); } else { return base.OnDisconnected(false); } } }
<script type="text/javascript"> var userHub = $.connection.userActivityHub; $.connection.hub.logging = true; $.connection.hub.start().done(function() { userHub.server.join(); }); $(function() { window.onbeforeunload = function() { $.connection.hub.stop(); }; }); </script>
<script type="text/javascript"> var userHub = $.connection.userActivityHub; userHub.client.showUserCount = function (message) { $('#userOnlineCount').html(message); }; $.connection.hub.start().done(function() { userHub.server.adminJoin().done(function() { userHub.server.getUserCount(); }); }); </script>
<script> $('a.btn.btn-danger.btn-block').click(function(e) { e.preventDefault(); $('#logoutForm').submit(); $.connection.userActivityHub.connection.stop(); }); $(function() { window.onbeforeunload = function(e) { $.connection.hub.stop(); }; }); </script>