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>
public class Student { [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("name")] public string Name { get; set; } } public class WeatherForecast { [Required] public int TemperatureC { get; set; } [MinLength(50)] public string Summary { get; set; } }
روش متداول ارسال نوعها به attributes تا پیش از C# 11
تا پیش از C# 11، روش پیاده سازی یک attribute جنریک که بتواند با انواع و اقسام نوعها کار کند، به صورت زیر است:
- ارسال یک پارامتر از نوع System.Type به سازندهی attribute
- تعریف خاصیتی مانند ParamType در صورت نیاز؛ تا مشخص کند که چه نوعی به سازندهی attribute ارسال شدهاست. مانند مثال فرضی زیر:
[AttributeUsage(AttributeTargets.Class)] public class CustomDoNothingAttribute: Attribute { // Note the type parameter in the constructor public CustomDoNothingAttribute(Type t) { ParamType = t; } public Type ParamType { get; } }
[CustomDoNothing(typeof(string))] public class Student { public int Id { get; set; } public string Name { get; set; } }
امکان تعریف ویژگیهای جنریک در C# 11
C# 11 به همراه پیشتیبانی از generic attributes ارائه شدهاست. بنابراین اینبار بجای ارسال پارمتری از نوع Type به سازندهی ویژگی، میتوان کلاس آن attribute را به صورت جنریک تعریف کنیم که میتواند یک یا چندین نوع را به عنوان پارامتر بپذیرد. بنابراین مثال قبل در C# 11 به صورت زیر بازنویسی میشود:
[AttributeUsage(AttributeTargets.Class)] public class CustomDoNothingAttribute<T> : Attribute where T : class { public T ParamType { get; } } [CustomDoNothing<string>] public class Student { public int Id { get; set; } public string Name { get; set; } }
و اگر نیاز به تعیین چند نوع بود، باید خاصیت AllowMultiple نحوهی استفاده از ویژگی را به true تنظیم کرد:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class DecorateAttribute<T> : Attribute where T : class { // .... }
[Decorate<LoggerDecorator>] [Decorate<TimerDecorator>] public class SimpleWorker { // .... }
محدودیتهای انتخاب نوعها در ویژگیهای جنریک C# 11
در ویژگیهای جنریک نمیتوان از نوعهای زیر استفاده کرد (همان محدودیتهای typeof، در اینجا هم برقرار هستند):
- نوعهای dynamic
- nullable reference types مانند ?string
- نوعهای tuple تعریف شدهی به کمک C# tuple syntax مانند (int x, int y)
چون این نوعها به همراه یکسری metadata annotations هستند که صرفا بیانگر توضیحی اضافی در مورد نوع بکارگرفته شده هستند و در صورت نیاز، بجای آنها میتوانید از نوعهای زیر استفاده کنید:
- از object بجای dynamic
- از string بجای ?string
- از <ValueTuple<int, int بجای (int X, int Y)
همچنین در زمان استفادهی از یک ویژگی جنریک، باید نوع مورد استفاده، کاملا مشخص و در اصطلاح fully constructed باشد:
public class GenericAttribute<T> : Attribute { } public class GenericType<T> { [GenericAttribute<T>] // Not allowed! generic attributes must be fully constructed types. public string Method1() => default; [GenericAttribute<string>] public string Method2() => default; }
بدین منظور فریم ورک ASP.NET Web API کتابخانه ای برای تولید خودکار صفحات راهنما در زمان اجرا (run-time) فراهم کرده است.
ایجاد صفحات راهنمای API
برای شروع ابتدا ابزار ASP.NET and Web Tools 2012.2 Update را نصب کنید. اگر از ویژوال استودیو 2013 استفاده میکنید این ابزار بصورت خودکار نصب شده است. این ابزار صفحات راهنما را به قالب پروژههای ASP.NET Web API اضافه میکند.
یک پروژه جدید از نوع ASP.NET MVC Application بسازید و قالب Web API را برای آن انتخاب کنید. این قالب پروژه کنترلری بنام ValuesController را بصورت خودکار برای شما ایجاد میکند. همچنین صفحات راهنمای API هم برای شما ساخته میشوند. تمام کد مربوط به صفحات راهنما در قسمت Areas قرار دارند.
اگر اپلیکیشن را اجرا کنید خواهید دید که صفحه اصلی لینکی به صفحه راهنمای API دارد. از صفحه اصلی، مسیر تقریبی Help/ خواهد بود.
این لینک شما را به یک صفحه خلاصه (summary) هدایت میکند.
نمای این صفحه در مسیر Areas/HelpPage/Views/Help/Index.cshtml قرار دارد. میتوانید این نما را ویرایش کنید و مثلا قالب، عنوان، استایلها و دیگر موارد را تغییر دهید.
بخش اصلی این صفحه متشکل از جدولی است که APIها را بر اساس کنترلر طبقه بندی میکند. مقادیر این جدول بصورت خودکار و توسط اینترفیس IApiExplorer تولید میشوند. در ادامه مقاله بیشتر درباره این اینترفیس صحبت خواهیم کرد. اگر کنترلر جدیدی به API خود اضافه کنید، این جدول بصورت خودکار در زمان اجرا بروز رسانی خواهد شد.
ستون "API" متد HTTP و آدرس نسبی را لیست میکند. ستون "Documentation" مستندات هر API را نمایش میدهد. مقادیر این ستون در ابتدا تنها placeholder-text است. در ادامه مقاله خواهید دید چگونه میتوان از توضیحات XML برای تولید مستندات استفاده کرد.
هر API لینکی به یک صفحه جزئیات دارد، که در آن اطلاعات بیشتری درباره آن قابل مشاهده است. معمولا مثالی از بدنههای درخواست و پاسخ هم ارائه میشود.
افزودن صفحات راهنما به پروژه ای قدیمی
می توانید با استفاده از NuGet Package Manager صفحات راهنمای خود را به پروژههای قدیمی هم اضافه کنید. این گزینه مخصوصا هنگامی مفید است که با پروژه ای کار میکنید که قالب آن Web API نیست.
از منوی Tools گزینههای Library Package Manager, Package Manager Console را انتخاب کنید. در پنجره Package Manager Console فرمان زیر را وارد کنید.
Install-Package Microsoft.AspNet.WebApi.HelpPage
@Html.ActionLink("API", "Index", "Help", new { area = "" }, null)
همانطور که مشاهده میکنید مسیر نسبی صفحات راهنما "Help/" میباشد. همچنین اطمینان حاصل کنید که ناحیهها (Areas) بدرستی رجیستر میشوند. فایل Global.asax را باز کنید و کد زیر را در صورتی که وجود ندارد اضافه کنید.
protected void Application_Start() { // Add this code, if not present. AreaRegistration.RegisterAllAreas(); // ... }
افزودن مستندات API
بصورت پیش فرض صفحات راهنما از placeholder-text برای مستندات استفاده میکنند. میتوانید برای ساختن مستندات از توضیحات XML استفاده کنید. برای فعال سازی این قابلیت فایل Areas/HelpPage/App_Start/HelpPageConfig.cs را باز کنید و خط زیر را از حالت کامنت درآورید:
config.SetDocumentationProvider(new XmlDocumentationProvider( HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));
زیر قسمت Output گزینه XML documentation file را تیک بزنید و در فیلد روبروی آن مقدار "App_Data/XmlDocument.xml" را وارد کنید.
حال کنترلر ValuesController را از مسیر Controllers/ValuesController.cs/ باز کنید و یک سری توضیحات XML به متدهای آن اضافه کنید. بعنوان مثال:
/// <summary> /// Gets some very important data from the server. /// </summary> public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } /// <summary> /// Looks up some data by ID. /// </summary> /// <param name="id">The ID of the data.</param> public string Get(int id) { return "value"; }
اپلیکیشن را مجددا اجرا کنید و به صفحات راهنما بروید. حالا مستندات API شما باید تولید شده و نمایش داده شوند.
صفحات راهنما مستندات شما را در زمان اجرا از توضیحات XML استخراج میکنند. دقت کنید که هنگام توزیع اپلیکیشن، فایل XML را هم منتشر کنید.
توضیحات تکمیلی
صفحات راهنما توسط کلاس ApiExplorer تولید میشوند، که جزئی از فریم ورک ASP.NET Web API است. به ازای هر API این کلاس یک ApiDescription دارد که توضیحات لازم را در بر میگیرد. در اینجا منظور از "API" ترکیبی از متدهای HTTP و مسیرهای نسبی است. بعنوان مثال لیست زیر تعدادی API را نمایش میدهد:
- GET /api/products
- {GET /api/products/{id
- POST /api/products
اگر اکشنهای کنترلر از متدهای متعددی پشتیبانی کنند، ApiExplorer هر متد را بعنوان یک API مجزا در نظر خواهد گرفت. برای مخفی کردن یک API از ApiExplorer کافی است خاصیت ApiExplorerSettings را به اکشن مورد نظر اضافه کنید و مقدار خاصیت IgnoreApi آن را به true تنظیم نمایید.
[ApiExplorerSettings(IgnoreApi=true)] public HttpResponseMessage Get(int id) { }
همچنین میتوانید این خاصیت را به کنترلرها اضافه کنید تا تمام کنترلر از ApiExplorer مخفی شود.
کلاس ApiExplorer متن مستندات را توسط اینترفیس IDocumentationProvider دریافت میکند. کد مربوطه در مسیر Areas/HelpPage/XmlDocumentation.cs/ قرار دارد. همانطور که گفته شد مقادیر مورد نظر از توضیحات XML استخراج میشوند. نکته جالب آنکه میتوانید با پیاده سازی این اینترفیس مستندات خود را از منبع دیگری استخراج کنید. برای اینکار باید متد الحاقی SetDocumentationProvider را هم فراخوانی کنید، که در HelpPageConfigurationExtensions تعریف شده است.
کلاس ApiExplorer بصورت خودکار اینترفیس IDocumentationProvider را فراخوانی میکند تا مستندات APIها را دریافت کند. سپس مقادیر دریافت شده را در خاصیت Documentation ذخیره میکند. این خاصیت روی آبجکتهای ApiDescription و ApiParameterDescription تعریف شده است.
مطالعه بیشتر
در مورد بررسی ارتباط با دادهها در WPF باید سه مورد را بشناسیم:
- DataContext: این شیء اتصالش را به منبع دادهها برقرار کرده و هر موقع دادهای را نیاز داریم، از طریق این شیء تامین میشود.
- DataBinding: یک واسطه بین DataContext و هر آن چیزی است که قرار است از دادهها تغذیه کند. در تعریفی رسمیتر میگوییم: روشی ساده و قدرتمند بوده و واسطی است بین مدل تجاری و رابط کاربری. هر زمانی که دادهای تغییر کند، ما را آگاه میسازد که میتواند یک ارتباط یک طرفه یا دو طرفه باشد.
- DataTemplate: نحوهی فرمت بندی و نمایش دادهها را تعیین میکند.
ابتدا قبل از هر چیزی کلاس فرم قبلی را پیاده سازی میکنیم. در این پیاده سازی از یک enum برای انتخاب زمینههای کاری هم کمک گرفته ایم و هچنین با یک متد ایستا، منبع دادهی تک رکوردی را جهت تست برنامه آماده کردهایم:
public enum FieldOfWork { Actor=0, Director=1, Producer=2 } public class Person { public string Name { get; set; } public bool Gender { get; set; } public string ImageName { get; set; } public string Country { get; set; } public DateTime Date { get; set; } public IList<FieldOfWork> FieldOfWork { get; set; } public static Person GetPerson() { return new Person() { Name = "Leo", Gender = true, ImageName ="man.jpg", Country = "Italy", Date = DateTime.Now }; } }
حالا لازم است که این منبع داده را در اختیار DataContext بگذاریم. وارد بخش کد نویسی شده و در سازندهی پنجره کد زیر را مینویسیم:
DataContext = Person.GetPerson();
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = Person.GetPerson(); } }
همانطور که میدانید متن کنترل TextBox توسط خصوصیت Text پر میشود و برای همین در این خصوصیت مینویسیم:
Text="{Binding Name}"
Source="{Binding ImageName}"
اطلاع از به روزرسانی در منبع دادهها:
حال این نکته پیش میآید که اگر همین اطلاعات دریافت شده در مدل منبع داده تغییر کند، چگونه میتوانیم از این موضوع مطلع شده و همین اطلاعات به روز شده را که نمایش دادهایم، تغییر دهیم. بنابراین جهت اطلاع از این مورد، کد را به شکل زیر تغییر میدهیم.
کار را از یک کلاس آغاز میکنیم. از اینترفیس INotifyPropertyChanged ارث بری کرده و در آن یک رویداد و یک متد را تعریف میکنیم و کمی در هم در تعریف Propertyها دست میبریم. فعلا اینکار را فقط برای پراپرتی Name انجام میدهیم:
private string _name; public string Name { get { return _name; } set { _name = value; OnPropertyChanged("Name"); } } public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string property) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(property)); } }
در بخش Setter آن خصوصیت هم باید این متد را صدا زده و نام خصوصیت را به آن پاس بدهیم تا موقعی که مدل تغییر پیدا کرد، بگوید که خصوصیت Name بوده است که تغییر کرده است.
برای اینکه بدانیم کد واقعا کار میکند و تستی بر آن زده باشیم، فعلا دکمهی Save را به Change تغییر میدهیم و کد داخل پنجره را بدین صورت تغییر میدهیم:
public partial class MainWindow : Window { private Person person; public MainWindow() { InitializeComponent(); person = Person.GetPerson(); DataContext = person; } private void Button_Click(object sender, RoutedEventArgs e) { person.Name = "Leonardo Decaperio"; } }
این کد واقعا کدی مفید جهت به روزرسانی است ولی مشکلی دارد که نام پراپرتی باید به صورت String به آن پاس شود که در یک برنامه بزرگ این مورد یک مشکل خواهد شد و اگر نام خصوصیت تغییر کند باید نام داخل آن هم تغییر کند؛ پس کد را به شکل دیگری بازنویسی میکنیم:
private string _name; public string Name { get { return _name; } set { _name = value; OnPropertyChanged(); } } private void OnPropertyChanged([CallerMemberName] string property="") { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(property)); } }
OnPropertyChanged();
کد این قسمت
در قسمتهای آینده به بررسی تبدیل مقادیر و framework element و کنترلها میپردازیم.
public string Content { get; set; }
بنابراین اگر هدف، ثبت اطلاعات در فیلدی از نوع ntext در این بانک اطلاعاتی باشد باید به یکی از دو روش زیر عمل کرد:
[MaxLength] public string Content { get; set; }
[Column(TypeName = "ntext")] public string Text { get; set; }
بنابراین قید MaxLength بر روی خواصی که قرار است حاوی متونی طولانی باشند، میتواند به عنوان یک کار مفید جهت سازگاری با بانکهای مختلف، به شمار آید.
خواندنیهای 16 اردیبهشت
مثالی از خود مایکروسافت:
http://support.microsoft.com/kb/301240
در این حالت سیستم شما هم افزونه دیگری خواهد شد برای پایهای به نام forms authentication و مسلما نه به پختگی سیستم طراحی شده توسط مایکروسافت. زیاد به هجمههای سیاسی که پشت این شرکت هست توجه نکنید. سیستم membership آنرا که ملاحظه و تحلیل کنید متوجه خواهید شد این مسایل سرسری و سطحی طراحی نشده.
اگر هم بخواهید از سیستم membership استفاده کنید، میشود دیتابیس آنرا در محل دیتابیس خودتون ایجاد کنید. جداول شما در کنار جداول آن قرار خواهد گرفت. یعنی الزاما نیازی به دو دیتابیس مجزا نیست.
این مباحث رو در کتاب امنیت در ASP.Net 2.0 توضیح دادهام (قدم به قدم) و نیاز به ذکر چندین فصل در این مورد هست اگر بخواهم توضیح کامل و جامع بدهم:
http://naghoos-andisheh.ir/product_info.php?products_id=197
به صورت خلاصه:
از پایهای به نام Forms authentication استفاده کنید بدون نیاز به مباحث ASP.Net 2.0 که اساسا فقط یک افزونه هستند و نه بیشتر. سپس سیستم اعتبارسنجی خاص خودتون را بر اساس جداول موجود طراحی کنید.
یا اگر به دنبال سیستم پختهای هستید که توسط یک سری متخصص امنیتی طراحی شده، جدول خودتان را کنار بگذارید و به سیستم membership مایکروسافت آنرا ارتقاء دهید و باز هم تکرار میکنم این مورد اختیاری است.
نگاهی اجمالی به سورس:
BrockAllen.MembershipReboot BrockAllen.MembershipReboot.Ef BrockAllen.MembershipReboot .Repository
Membership.CreateUser(userName, password, email); Roles.AddUserToRoles(userName, "IdentityServerUsers");
[HttpPost] public ActionResult Register(RegisterModel model) { UserRepository.CreateUser(model.userName,model.password,model.email); Roles.AddUserToRoles(userName, "IdentityServerUsers"); return View(model); }
void CreateUser(string userName, string password, string email = null); void DeleteUser(string userName); IEnumerable<string> GetUsers(int start, int count, out int totalCount); IEnumerable<string> GetUsers(string filter, int start, int count, out int totalCount); void SetPassword(string userName, string password); void SetRolesForUser(string userName, IEnumerable<string> roles); IEnumerable<string> GetRolesForUser(string userName); IEnumerable<string> GetRoles(); void CreateRole(string roleName); void DeleteRole(string roleName);
چگونگی رسیدگی به Null property در AutoMapper
public class News : BaseEntity { public byte[] SmallImage { get; set; } // SmallImage }
news = model.ToEntity(news);
public static News ToEntity(this NewsModel model, News destination) { Mapper.CreateMap<NewsModel, News>(); return Mapper.Map(model, destination); }
برخلاف نگارشهای پیشین EF، اینبار Lazy loading به صورت پیشفرض فعال نیست که در بسیاری از موارد یک مزیت مهم، در جهت بهبود کارآیی برنامه به حساب میآید؛ چون پیشتر مدام میبایستی توسط ابزارهای profiler، برنامه را بررسی میکردیم تا از وجود مشکلی به نام select n+1 مطلع میشدیم (lazy loading اشتباه، در جائی که نیازی به آن نبوده و رفت و برگشت بیش از اندازهای را به بانک اطلاعاتی سبب شدهاست).
در این حالت ابتدا نیاز است بستهی نیوگت Microsoft.EntityFrameworkCore.Proxies را نصب کنید. سپس در متد OnConfiguring مربوط به Context برنامه، متد UseLazyLoadingProxies را فراخوانی نمائید:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseLazyLoadingProxies() .UseSqlServer(myConnectionString);
.AddDbContext<BloggingContext>( b => b.UseLazyLoadingProxies() .UseSqlServer(myConnectionString));
این خواص نیز حتما باید به صورت virtual معرفی شوند تا قابلیت بازنویسی را داشته باشند؛ مانند:
public class Blog { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<Post> Posts { get; set; } } public class Post { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public virtual Blog Blog { get; set; } }
هرچند این قابلیت بارگذاری اطلاعات وابسته در آینده، جذاب به نظر میرسد اما در عمل در حین رندر یک گرید و یا بکارگیری حلقهها، چون سبب رفت و برگشت بیش از اندازهای به بانک اطلاعاتی خواهد شد، باید با دقت مورد استفاده قرار گیرد و اساسا استفادهی از آن در برنامههای وب توصیه نمیشود (با بررسیهای پروژههای بسیاری مشخص شدهاست که این قابلیت ضررش بیشتر از نفعش است).
ب) فعالسازی Lazy loading بدون استفاده از Proxyها
در این حالت نیازی به نصب بستهی AOP جدید تشکیل پروکسیها نیست. در اینجا در کلاس موجودیت خود باید سرویس ILazyLoader را تزریق کنید:
public class Blog { private ICollection<Post> _posts; public Blog() { } private Blog(ILazyLoader lazyLoader) { LazyLoader = lazyLoader; } private ILazyLoader LazyLoader { get; set; } public int Id { get; set; } public string Name { get; set; } public ICollection<Post> Posts { get => LazyLoader?.Load(this, ref _posts); set => _posts = value; } } public class Post { private Blog _blog; public Post() { } private Post(ILazyLoader lazyLoader) { LazyLoader = lazyLoader; } private ILazyLoader LazyLoader { get; set; } public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get => LazyLoader?.Load(this, ref _blog); set => _blog = value; } }