/// <summary> /// دسترسیهای مستقیم کاربر بدون وابستی به گروههای کاربری او /// </summary> public string DirectPermissions { get; set; } /// <summary> /// ساختار اکس ام ال دسترسیهای مستقیم کاربر بدون وابستی به گروههای کاربری او /// </summary> public XElement XmlDirectPermissions { get { return XElement.Parse(DirectPermissions); } set { DirectPermissions = value.ToString(); } }
برای استفاده از FTP نیاز به یک اکانت FTP در سایت مورد نظر بهمراه دسترسی به پوشه ای مشخص میباشد.
برای مثال ما یک اکانت FTP در سایت dotnettip.info داریم که به پوشه upload دسترسی داره.
ابتدا در فایل Web.config و در بین تگ های appSettings مقادیر زیر را برای دسترسی به اکانت و نام کاربری و رمز عبور ذخیره میکنیم.
<add key="FtpAddress" value="ftp://ftp.dotnetips.info" /> <add key="FtpUser" value="uploadcenter" /> <add key="FtpPass" value="123123" /> <add key="FolderPath" value="~/Upload/" />
در ادامه یک کلاس در App_code پروژه خود با نام FTPHelper ایجاد میکنیم و کد زیر را در آن قرار میدهیم:
تکه کد بالا برای ست کردن مقادیر نام کاربری و رمز عبور و آدرس FTP در کلاس مذکور که بصورت پیشفرض از web.config پر میشود ایجاد و بکار خواهد رفت.
using System.Net; using System.IO; using System.Configuration; public class FtpHelper { public FtpHelper() { //Default Value Set From Application _hostname = ConfigurationManager.AppSettings.GetValues("FtpAddress")[0]; _username = ConfigurationManager.AppSettings.GetValues("FtpUser")[0]; _password = ConfigurationManager.AppSettings.GetValues("FtpPass")[0]; } #region "Properties" private string _hostname; /// <summary> /// Hostname /// </summary> /// <value></value> /// <remarks>Hostname can be in either the full URL format /// ftp://ftp.myhost.com or just ftp.myhost.com /// </remarks> public string Hostname { get { if (_hostname.StartsWith("ftp://")) { return _hostname; } else { return "ftp://" + _hostname; } } set { _hostname = value; } } private string _username; /// <summary> /// Username property /// </summary> /// <value></value> /// <remarks>Can be left blank, in which case 'anonymous' is returned</remarks> public string Username { get { return (_username == "" ? "anonymous" : _username); } set { _username = value; } } private string _password; public string Password { get { return _password; } set { _password = value; } } #endregion }
سپس فضای نامهای زیر را در کلاس خود قرار میدهیم.
using System.Net; using System.IO;
public static bool Upload(string fileUrl) { if (File.Exists(fileUrl)) { FtpHelper ftpClient = new FtpHelper(); string ftpUrl = ftpClient.Hostname + System.IO.Path.GetFileName(fileUrl); FtpWebRequest ftp = (FtpWebRequest)FtpWebRequest.Create(ftpUrl); ftp.Credentials = new NetworkCredential(ftpClient.Username, ftpClient.Password); ftp.KeepAlive = true; ftp.UseBinary = true; ftp.Timeout = 3600000; ftp.KeepAlive = true; ftp.Method = WebRequestMethods.Ftp.UploadFile; const int bufferLength = 102400; byte[] buffer = new byte[bufferLength]; int readBytes = 0; //open file for reading using (FileStream fs = File.OpenRead(fileUrl)) { try { //open request to send using (Stream rs = ftp.GetRequestStream()) { do { readBytes = fs.Read(buffer, 0, bufferLength); fs.Write(buffer, 0, readBytes); } while (!(readBytes < bufferLength)); rs.Close(); } } catch (Exception) { //Optional Alert for Exeption To Application Layer //throw (new ApplicationException("بارگذاری فایل با خطا رو به رو شد")); } finally { //ensure file closed //fs.Close(); } } ftp = null; return true; } return false; }
تکه کد بالا فایل مورد نظر را در صورت وجود به صورت تکههای 100 کیلوبایتی بر روی ftp بارگذاری میکند، که میتوانید مقدار آنرا نیز تغییر دهید.
اینکار باعث افزایش سرعت بارگذاری در فایلهای با حجم بالا برای بارگذاری میشود.
در بخشهای بعدی نحوه ایجاد پوشه ، حذف فایل ، حذف پوشه و دانلود فایل از روی FTP را بررسی خواهیم کرد.
البته همیشه درخواست کنترل این جدول واسط که کاملا از دیدگاه ORMها (تمام آنها) مخفی است، وجود داشتهاست و قرار است این پشتیبانی توسط مفهوم ویژهای به نام shadow properties به نگارشهای بعدی EF Core اضافه شود.
اما فعلا در نگارش اول آن، توصیه شدهاست که رابطهی many-to-many را به صورت دو رابطهی one-to-many مدلسازی کنید که در ادامه آنرا بررسی خواهیم کرد. بنابراین پیشنیاز آن مطالعهی مطلب «شروع به کار با EF Core 1.0 - قسمت 7 - بررسی رابطهی One-to-Many» میباشد.
مدلسازی موجودیتهای یک رابطهی چند به چند در EF Core 1.0 RTM توسط Fluent API
در اینجا نحوهی مدلسازی یک رابطهی چند به چند را توسط دو رابطهی one-to-many مشاهده میکنید. تنها تفاوت آن با EF 6.x، قید صریح جدول واسط BlogPostsJoinTags است که یک چنین جدولی در EF 6.x به صورت خودکار تشکیل شده و مدیریت میشود و کاملا از دید برنامه مخفی است. اما در اینجا (در نگارش اول EF Core) نیاز است این جدول واسط را از حالت مخفی خارج کرد و سپس دو رابطهی یک به چند را به جداول مطالب و تگهای آنها تشکیل داد.
مزیت این حالت، دسترسی کامل به طراحی جدول واسط، توسط برنامه است. بنابراین اگر به هر دلیلی نیاز به افزودن خواص بیشتری به این جدول ویژه دارید، اکنون امکان آن میسر است.
public class Tags { public Tags() { BlogPostsJoinTags = new HashSet<BlogPostsJoinTags>(); } public int Id { get; set; } public string Name { get; set; } public virtual ICollection<BlogPostsJoinTags> BlogPostsJoinTags { get; set; } }
public class BlogPostsJoinTags { public virtual BlogPosts BlogPost { get; set; } public int BlogPostId { get; set; } public virtual Tags Tag { get; set; } public int TagId { get; set; } }
public class BlogPosts { public BlogPosts() { BlogPostsJoinTags = new HashSet<BlogPostsJoinTags>(); } public int Id { get; set; } public string Title { get; set; } public string Body { get; set; } public virtual ICollection<BlogPostsJoinTags> BlogPostsJoinTags { get; set; } }
به علاوه در اینجا تعریف یک composite key را هم بر روی خواص کلید خارجی جدول واسط مشاهده میکنید. وجود این کلید ترکیبی سبب خواهد شد که ملزم به ثبت هر دو Id (کلیدهای جداول مطلب و تگ) در حین ثبت در این جدول شویم (یا قید اجباری هر دو طرف رابطه).
public class MyDBDataContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=testdb2;Integrated Security = true"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<BlogPosts>(entity => { entity.Property(e => e.Title) .IsRequired() .HasMaxLength(450); }); modelBuilder.Entity<Tags>(entity => { entity.Property(e => e.Name) .IsRequired() .HasMaxLength(450); }); modelBuilder.Entity<BlogPostsJoinTags>(entity => { entity.HasKey(e => new { e.TagId, e.BlogPostId }) .HasName("PK_dbo.BlogPostsJoinTags"); entity.HasIndex(e => e.BlogPostId) .HasName("IX_BlogPostId"); entity.HasIndex(e => e.TagId) .HasName("IX_TagId"); entity.HasOne(d => d.BlogPost) .WithMany(p => p.BlogPostsJoinTags) .HasForeignKey(d => d.BlogPostId) .HasConstraintName("FK_dbo.BlogPostsJoinTags_dbo.BlogPosts_BlogPostId"); entity.HasOne(d => d.Tag) .WithMany(p => p.BlogPostsJoinTags) .HasForeignKey(d => d.TagId) .HasConstraintName("FK_dbo.BlogPostsJoinTags_dbo.Tags_TagId"); }); } public virtual DbSet<BlogPosts> BlogPosts { get; set; } public virtual DbSet<BlogPostsJoinTags> BlogPostsJoinTags { get; set; } public virtual DbSet<Tags> Tags { get; set; } }
مدلسازی موجودیتهای یک رابطهی چند به چند در EF Core 1.0 RTM توسط Data Annotations
در حالت مدلسازی توسط ویژگیها، ذکر InversePropertyها و همچنین ForeignKeyها مقداری واضحتر به نظر میرسند. به علاوه، یک سری از تنظیمات هم معادل data annotations ایی ندارند؛ مانند composite key تعریف شدهی بر روی خواص جدول واسط و همچنین ایندکسهای تعریف شده، که حتما باید توسط Fluent API تنظیم شوند.
public class Tags { public Tags() { BlogPostsJoinTags = new HashSet<BlogPostsJoinTags>(); } public int Id { get; set; } [Required] [MaxLength(450)] public string Name { get; set; } [InverseProperty("Tag")] public virtual ICollection<BlogPostsJoinTags> BlogPostsJoinTags { get; set; } }
public class BlogPostsJoinTags { [ForeignKey("BlogPostId")] [InverseProperty("BlogPostsJoinTags")] public virtual BlogPosts BlogPost { get; set; } public int BlogPostId { get; set; } [ForeignKey("TagId")] [InverseProperty("BlogPostsJoinTags")] public virtual Tags Tag { get; set; } public int TagId { get; set; } }
public class BlogPosts { public BlogPosts() { BlogPostsJoinTags = new HashSet<BlogPostsJoinTags>(); } public int Id { get; set; } [Required] [MaxLength(450)] public string Title { get; set; } public string Body { get; set; } [InverseProperty("BlogPost")] public virtual ICollection<BlogPostsJoinTags> BlogPostsJoinTags { get; set; } }
public class MyDBDataContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=testdb2;Integrated Security = true"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<BlogPostsJoinTags>(entity => { entity.HasKey(e => new { e.TagId, e.BlogPostId }) .HasName("PK_dbo.BlogPostsJoinTags"); entity.HasIndex(e => e.BlogPostId) .HasName("IX_BlogPostId"); entity.HasIndex(e => e.TagId) .HasName("IX_TagId"); }); } public virtual DbSet<BlogPosts> BlogPosts { get; set; } public virtual DbSet<BlogPostsJoinTags> BlogPostsJoinTags { get; set; } public virtual DbSet<Tags> Tags { get; set; } }
نحوهی ثبت اطلاعات در دو رابطهی یک به چند به همراه جدول واسط
در EF 6.x، کار مقدار دهی Idهای جدول واسط به صورت خودکار انجام میشود. در اینجا این مقدار دهی را باید به صورت صریح انجام داد:
var post = new BlogPosts { ... }; context.BlogPosts.Add(post); var tag = new Tags { ... }; context.Tags.Add(tag); var postTag = new BlogPostsJoinTags { Tag = tag, BlogPost = post }; context.PostsTags.Add(postTag); context.SaveChanges();
نحوهی واکشی اطلاعات به هم مرتبط در دو رابطهی یک به چند به همراه جدول واسط
در مورد متدهای Include و ThenInclude در مطلب «شروع به کار با EF Core 1.0 - قسمت 7 - بررسی رابطهی One-to-Many» پیشتر بحث شد.
BlogPosts post1 = this.BlogPosts .Include(blogPosts => blogPosts.BlogPostsJoinTags) .ThenInclude(joinTags => joinTags.Tag) .First(blogPosts => blogPosts.Id == 1); IEnumerable<Tags> post1Tags = post1.BlogPostsJoinTags.Select(x => x.Tag);
یک نکتهی تکمیلی
وضعیت پشتیبانی از رابطهی many-to-many را همانند EF 6.x در EF Core، در اینجا میتوانید پیگیری کنید.
ایجاد یک Repository در پروژه برای دستورات EF
using System; using System.Collections; using System.Linq; namespace Framework.Model { public interface IContext { T Get<T>(Func<T, bool> prediction) where T : class; IEnumerable List<T>(Func<T, bool> prediction) where T : class; void Insert<T>(T entity) where T : class; int Save(); } }
using System; using System.Collections; using System.Collections.Generic; using System.Data; using System.Data.Entity; using System.Linq; using System.Text; namespace Framework.Model { public class Context : IContext { private readonly DbContext _dbContext; public Context(DbContext context) { _dbContext = context; } public T Get<T>(Func<T,bool> prediction) where T : class { var dbSet = _dbContext.Set<T>(); if (dbSet!= null) return dbSet.Single(prediction); throw new Exception(); } public void Insert<T>(T entity) where T : class { var dbSet = _dbContext.Set<T>(); if (dbSet != null) { _dbContext.Entry(entity).State = EntityState.Added; } } public int Save() { return _dbContext.SaveChanges(); } IEnumerable IContext.List<T>(Func<T, bool> prediction) { var dbSet = _dbContext.Set<T>(); if (dbSet != null) return dbSet.Where(prediction).ToList(); throw new Exception(); } } }
using System.Data.Entity; using DataModel; namespace Model { public class EFContext : DbContext { public EFContext(string db): base(db) { } public DbSet<Product> Products { get; set; } } }
using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Text; namespace Model { public class Context : Framework.Model.Context { public Context(string db): base(new EFContext(db)) { } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Biz { public class Context : Model.Context { public Context(string db) : base(db) { } } }
using System.Web.Mvc; using Framework.Model; namespace ProductionRepository.Controllers { public class BaseController : Controller { public IContext DataContext { get; set; } public BaseController() { DataContext = new Biz.Context(System.Configuration.ConfigurationManager.ConnectionStrings["Database"].ConnectionString); } } }
using System.Web.Mvc; using DataModel; using System.Collections.Generic; namespace ProductionRepository.Controllers { public class ProductController : BaseController { public ActionResult Index() { var x = DataContext.List<Product>(s => s.Name != null); return View(x); } } }
using NUnit.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Mvc; namespace TestUnit { [TestFixture] public class Test { [Test] public void IndexShouldListProduct() { var repo = new Moq.Mock<Framework.Model.IContext>(); var products = new List<DataModel.Product>(); products.Add(new DataModel.Product { Id = 1, Name = "asdasdasd" }); products.Add(new DataModel.Product { Id = 2, Name = "adaqwe" }); products.Add(new DataModel.Product { Id = 4, Name = "qewqw" }); products.Add(new DataModel.Product { Id = 5, Name = "qwe" }); repo.Setup(x => x.List<DataModel.Product>(p => p.Name != null)).Returns(products.AsEnumerable()); var controller = new ProductionRepository.Controllers.ProductController(); controller.DataContext = repo.Object; var result = controller.Index() as ViewResult; var model = result.Model as List<DataModel.Product>; Assert.AreEqual(4, model.Count); Assert.AreEqual("", result.ViewName); } } }
Blazor 5x - قسمت 25 - تهیه API مخصوص Blazor WASM - بخش 2 - تامین پایهی اعتبارسنجی و احراز هویت
- «معرفی JSON Web Token»
توسعهی IdentityUser
در قسمتهای 21 تا 23، روش نصب و یکپارچگی ASP.NET Core Identity را با یک برنامهی Blazor Server بررسی کردیم. در پروژهی Web API جاری هم از قصد داریم از ASP.NET Core Identity استفاده کنیم؛ البته بدون نصب UI پیشفرض آن. به همین جهت فقط از ApplicationDbContext آن برنامه که از IdentityDbContext مشتق شده و همچنین قسمتی از تنظیمات سرویسهای ابتدایی آن که در قسمت قبل بررسی کردیم، در اینجا استفاده خواهیم کرد.
IdentityUser پیشفرض که معرف موجودیت کاربران یک سیستم مبتنی بر ASP.NET Core Identity است، برای ثبت نام یک کاربر، فقط به ایمیل و کلمهی عبور او نیاز دارد که نمونهای از آنرا در حین معرفی «ثبت کاربر ادمین Identity» بررسی کردیم. اکنون میخواهیم این موجودیت پیشفرض را توسعه داده و برای مثال نام کاربر را نیز به آن اضافه کنیم. برای اینکار فایل جدید BlazorServer\BlazorServer.Entities\ApplicationUser .cs را به پروژهی Entities با محتوای زیر اضافه میکنیم:
using Microsoft.AspNetCore.Identity; namespace BlazorServer.Entities { public class ApplicationUser : IdentityUser { public string Name { get; set; } } }
اکنون که یک ApplicationUser سفارشی را ایجاد کردیم، نیازی نیست تا DbSet خاص آنرا به ApplicationDbContext برنامه اضافه کنیم. برای معرفی آن به برنامه ابتدا باید به فایل BlazorServer\BlazorServer.DataAccess\ApplicationDbContext.cs مراجعه کرده و نوع IdentityUser را به IdentityDbContext، از طریق آرگومان جنریکی که میپذیرد، معرفی کنیم:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
پس از این تغییر، در فایل BlazorWasm\BlazorWasm.WebApi\Startup.cs نیز باید ApplicationUser را به عنوان نوع جدید کاربران، معرفی کرد:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); // ...
پس از این تغییرات، باید از طریق خط فرمان به پوشهی BlazorServer.DataAccess وارد شد و دستورات زیر را جهت ایجاد و اعمال Migrations متناظر با تغییرات فوق، اجرا کرد. چون در این دستورات اینبار پروژهی آغازین، به پروژهی Web API اشاره میکند، باید بستهی نیوگت Microsoft.EntityFrameworkCore.Design را نیز به پروژهی آغازین اضافه کرد، تا بتوان آنها را با موفقیت به پایان رساند:
dotnet tool update --global dotnet-ef --version 5.0.4 dotnet build dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddNameToAppUser --context ApplicationDbContext dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
ایجاد مدلهای ثبت نام
در ادامه میخواهیم کنترلری را ایجاد کنیم که کار ثبت نام و لاگین را مدیریت میکند. برای این منظور باید بتوان از کاربر، اطلاعاتی مانند نام کاربری و کلمهی عبور او را دریافت کرد و پس از پایان عملیات نیز نتیجهی آنرا بازگشت داد. به همین جهت دو مدل زیر را جهت مدیریت قسمت ثبت نام، به پروژهی BlazorServer.Models اضافه میکنیم:
using System.ComponentModel.DataAnnotations; namespace BlazorServer.Models { public class UserRequestDTO { [Required(ErrorMessage = "Name is required")] public string Name { get; set; } [Required(ErrorMessage = "Email is required")] [RegularExpression("^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$", ErrorMessage = "Invalid email address")] public string Email { get; set; } public string PhoneNo { get; set; } [Required(ErrorMessage = "Password is required.")] [DataType(DataType.Password)] public string Password { get; set; } [Required(ErrorMessage = "Confirm password is required")] [DataType(DataType.Password)] [Compare("Password", ErrorMessage = "Password and confirm password is not matched")] public string ConfirmPassword { get; set; } } }
public class RegistrationResponseDTO { public bool IsRegistrationSuccessful { get; set; } public IEnumerable<string> Errors { get; set; } }
ایجاد و تکمیل کنترلر Account، جهت ثبت نام کاربران
در ادامه نیاز داریم تا جهت ارائهی امکانات اعتبارسنجی و احراز هویت کاربران، کنترلر جدید Account را به پروژهی Web API اضافه کنیم:
using System; using BlazorServer.Entities; using BlazorServer.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using System.Linq; using BlazorServer.Common; namespace BlazorWasm.WebApi.Controllers { [Route("api/[controller]/[action]")] [ApiController] [Authorize] public class AccountController : ControllerBase { private readonly SignInManager<ApplicationUser> _signInManager; private readonly UserManager<ApplicationUser> _userManager; private readonly RoleManager<IdentityRole> _roleManager; public AccountController(SignInManager<ApplicationUser> signInManager, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager) { _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager)); _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager)); } [HttpPost] [AllowAnonymous] public async Task<IActionResult> SignUp([FromBody] UserRequestDTO userRequestDTO) { var user = new ApplicationUser { UserName = userRequestDTO.Email, Email = userRequestDTO.Email, Name = userRequestDTO.Name, PhoneNumber = userRequestDTO.PhoneNo, EmailConfirmed = true }; var result = await _userManager.CreateAsync(user, userRequestDTO.Password); if (!result.Succeeded) { var errors = result.Errors.Select(e => e.Description); return BadRequest(new RegistationResponseDTO { Errors = errors, IsRegistrationSuccessful = false }); } var roleResult = await _userManager.AddToRoleAsync(user, ConstantRoles.Customer); if (!roleResult.Succeeded) { var errors = result.Errors.Select(e => e.Description); return BadRequest(new RegistationResponseDTO { Errors = errors, IsRegistrationSuccessful = false }); } return StatusCode(201); // Created } } }
- در تعریف ابتدایی این کنترلر، ویژگیهای زیر ذکر شدهاند:
[Route("api/[controller]/[action]")] [ApiController] [Authorize]
تا اینجا اگر برنامه را اجرا کنیم، میتوان با استفاده از Swagger UI، آنرا آزمایش کرد:
که با اجرای آن، برای نمونه به خروجی زیر میرسیم:
که عنوان میکند کلمهی عبور باید حداقل دارای یک عدد و یک حرف بزرگ باشد. پس از اصلاح آن، status-code=201 را دریافت خواهیم کرد.
و اگر سعی کنیم همین کاربر را مجددا ثبت نام کنیم، با خطای زیر مواجه خواهیم شد:
ایجاد مدلهای ورود به سیستم
در پروژهی Web API، از UI پیشفرض ASP.NET Core Identity استفاده نمیکنیم. به همین جهت نیاز است مدلهای قسمت لاگین را به صورت زیر تعریف کنیم:
using System.ComponentModel.DataAnnotations; namespace BlazorServer.Models { public class AuthenticationDTO { [Required(ErrorMessage = "UserName is required")] [RegularExpression("^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$", ErrorMessage = "Invalid email address")] public string UserName { get; set; } [Required(ErrorMessage = "Password is required.")] [DataType(DataType.Password)] public string Password { get; set; } } }
using System.Collections.Generic; namespace BlazorServer.Models { public class AuthenticationResponseDTO { public bool IsAuthSuccessful { get; set; } public string ErrorMessage { get; set; } public string Token { get; set; } public UserDTO UserDTO { get; set; } } public class UserDTO { public string Id { get; set; } public string Name { get; set; } public string Email { get; set; } public string PhoneNo { get; set; } } }
ایجاد مدل مشخصات تولید JSON Web Token
پس از لاگین موفق، نیاز است یک JWT را تولید کرد و در اختیار کلاینت قرار داد. مشخصات ابتدایی تولید این توکن، توسط مدل زیر تعریف میشود:
namespace BlazorServer.Models { public class BearerTokensOptions { public string Key { set; get; } public string Issuer { set; get; } public string Audience { set; get; } public int AccessTokenExpirationMinutes { set; get; } } }
{ "BearerTokens": { "Key": "This is my shared key, not so secret, secret!", "Issuer": "https://localhost:5001/", "Audience": "Any", "AccessTokenExpirationMinutes": 20 } }
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddOptions<BearerTokensOptions>().Bind(Configuration.GetSection("BearerTokens")); // ...
ایجاد سرویسی برای تولید JSON Web Token
سرویس زیر به کمک سرویس توکار UserManager مخصوص Identity و مشخصات ابتدایی توکنی که معرفی کردیم، کار تولید یک JWT را انجام میدهد:
using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using BlazorServer.Entities; using BlazorServer.Models; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace BlazorServer.Services { public interface ITokenFactoryService { Task<string> CreateJwtTokensAsync(ApplicationUser user); } public class TokenFactoryService : ITokenFactoryService { private readonly UserManager<ApplicationUser> _userManager; private readonly BearerTokensOptions _configuration; public TokenFactoryService( UserManager<ApplicationUser> userManager, IOptionsSnapshot<BearerTokensOptions> bearerTokensOptions) { _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); if (bearerTokensOptions is null) { throw new ArgumentNullException(nameof(bearerTokensOptions)); } _configuration = bearerTokensOptions.Value; } public async Task<string> CreateJwtTokensAsync(ApplicationUser user) { var signingCredentials = new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Key)), SecurityAlgorithms.HmacSha256); var claims = await getClaimsAsync(user); var now = DateTime.UtcNow; var tokenOptions = new JwtSecurityToken( issuer: _configuration.Issuer, audience: _configuration.Audience, claims: claims, notBefore: now, expires: now.AddMinutes(_configuration.AccessTokenExpirationMinutes), signingCredentials: signingCredentials); return new JwtSecurityTokenHandler().WriteToken(tokenOptions); } private async Task<List<Claim>> getClaimsAsync(ApplicationUser user) { string issuer = _configuration.Issuer; var claims = new List<Claim> { // Issuer new Claim(JwtRegisteredClaimNames.Iss, issuer, ClaimValueTypes.String, issuer), // Issued at new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64, issuer), new Claim(ClaimTypes.Name, user.Email, ClaimValueTypes.String, issuer), new Claim(ClaimTypes.Email, user.Email, ClaimValueTypes.String, issuer), new Claim("Id", user.Id, ClaimValueTypes.String, issuer), new Claim("DisplayName", user.Name, ClaimValueTypes.String, issuer), }; var roles = await _userManager.GetRolesAsync(user); foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role, ClaimValueTypes.String, issuer)); } return claims; } } }
در آخر، این سرویس را به صورت زیر به لیست سرویسهای ثبت شدهی پروژهی Web API، اضافه میکنیم:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddScoped<ITokenFactoryService, TokenFactoryService>(); // ...
تکمیل کنترلر Account جهت لاگین کاربران
پس از ثبت نام کاربران، اکنون میخواهیم امکان لاگین آنها را نیز فراهم کنیم:
namespace BlazorWasm.WebApi.Controllers { [Route("api/[controller]/[action]")] [ApiController] [Authorize] public class AccountController : ControllerBase { private readonly SignInManager<ApplicationUser> _signInManager; private readonly UserManager<ApplicationUser> _userManager; private readonly ITokenFactoryService _tokenFactoryService; public AccountController( SignInManager<ApplicationUser> signInManager, UserManager<ApplicationUser> userManager, ITokenFactoryService tokenFactoryService) { _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager)); _tokenFactoryService = tokenFactoryService; } [HttpPost] [AllowAnonymous] public async Task<IActionResult> SignIn([FromBody] AuthenticationDTO authenticationDTO) { var result = await _signInManager.PasswordSignInAsync( authenticationDTO.UserName, authenticationDTO.Password, isPersistent: false, lockoutOnFailure: false); if (!result.Succeeded) { return Unauthorized(new AuthenticationResponseDTO { IsAuthSuccessful = false, ErrorMessage = "Invalid Authentication" }); } var user = await _userManager.FindByNameAsync(authenticationDTO.UserName); if (user == null) { return Unauthorized(new AuthenticationResponseDTO { IsAuthSuccessful = false, ErrorMessage = "Invalid Authentication" }); } var token = await _tokenFactoryService.CreateJwtTokensAsync(user); return Ok(new AuthenticationResponseDTO { IsAuthSuccessful = true, Token = token, UserDTO = new UserDTO { Name = user.Name, Id = user.Id, Email = user.Email, PhoneNo = user.PhoneNumber } }); } } }
تا اینجا اگر برنامه را اجرا کنیم، میتوان در قسمت ورود به سیستم، برای نمونه مشخصات کاربر ادمین را وارد کرد:
و پس از اجرای درخواست، به خروجی زیر میرسیم:
که در اینجا JWT تولید شدهی به همراه قسمتی از مشخصات کاربر، در خروجی نهایی مشخص است. میتوان محتوای این توکن را در سایت jwt.io مورد بررسی قرار داد که به این خروجی میرسیم و حاوی claims تعریف شدهاست:
{ "iss": "https://localhost:5001/", "iat": 1616396383, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "vahid@dntips.ir", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "vahid@dntips.ir", "Id": "582855fb-e95b-45ab-b349-5e9f7de40c0c", "DisplayName": "vahid@dntips.ir", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", "nbf": 1616396383, "exp": 1616397583, "aud": "Any" }
تنظیم Web API برای پذیرش و پردازش JWT ها
تا اینجا پس از لاگین، یک JWT را در اختیار کلاینت قرار میدهیم. اما اگر کلاینت این JWT را به سمت سرور ارسال کند، اتفاق خاصی رخ نخواهد داد و توسط آن، شیء User قابل دسترسی در یک اکشن متد، به صورت خودکار تشکیل نمیشود. برای رفع این مشکل، ابتدا بستهی جدید نیوگت Microsoft.AspNetCore.Authentication.JwtBearer را به پروژهی Web API اضافه میکنیم، سپس به کلاس آغازین پروژهی Web API مراجعه کرده و آنرا به صورت زیر تکمیل میکنیم:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { var bearerTokensSection = Configuration.GetSection("BearerTokens"); services.AddOptions<BearerTokensOptions>().Bind(bearerTokensSection); // ... var apiSettings = bearerTokensSection.Get<BearerTokensOptions>(); var key = Encoding.UTF8.GetBytes(apiSettings.Key); services.AddAuthentication(opt => { opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(cfg => { cfg.RequireHttpsMetadata = false; cfg.SaveToken = true; cfg.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateAudience = true, ValidateIssuer = true, ValidAudience = apiSettings.Audience, ValidIssuer = apiSettings.Issuer, ClockSkew = TimeSpan.Zero, ValidateLifetime = true }; }); // ...
افزودن JWT به تنظیمات Swagger
هر کدام از اکشن متدهای کنترلرهای Web API برنامه که مزین به فیلتر Authorize باشد، در Swagger UI با یک قفل نمایش داده میشود. در این حالت میتوان این UI را به نحو زیر سفارشی سازی کرد تا بتواند JWT را دریافت و به سمت سرور ارسال کند:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { // ... services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "BlazorWasm.WebApi", Version = "v1" }); c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter the token in the field", Name = "Authorization", Type = SecuritySchemeType.ApiKey }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] { } } }); }); } // ...
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-25.zip
تنظیم Ember data برای کار با سرور
Ember data به صورت پیش فرض و در پشت صحنه با استفاده از Ajax برای کار با یک REST Web Service طراحی شدهاست و کلیه تبادلات آن نیز با فرمت JSON انجام میشود. بنابراین تمام کدهای سمت کاربر قسمت قبل نیز در این حالت کار خواهند کرد. تنها کاری که باید انجام شود، حذف تنظیمات ابتدایی آن برای کار با HTML 5 local storage است.
برای این منظور ابتدا فایل index.html را گشوده و سپس مدخل localstorage_adapter.js را از آن حذف کنید:
<!--<script src="Scripts/Libs/localstorage_adapter.js" type="text/javascript"></script>-->
<!--<script src="Scripts/App/store.js" type="text/javascript"></script>-->
اکنون برنامه را اجرا کنید، چنین پیام خطایی را مشاهده خواهید کرد:
همانطور که عنوان شد، ember data به صورت پیش فرض با سرور کار میکند و در اینجا به صورت خودکار، یک درخواست Get را به آدرس http://localhost:25918/posts جهت دریافت آخرین مطالب ثبت شده، ارسال کردهاست و چون هنوز وب سرویسی در برنامه تعریف نشده، با خطای 404 و یا یافت نشد، مواجه شدهاست.
این درخواست نیز بر اساس تعاریف موجود در فایل Scripts\Routes\posts.js، به سرور ارسال شدهاست:
Blogger.PostsRoute = Ember.Route.extend({ model: function () { return this.store.find('post'); } });
تغییر تنظیمات پیش فرض آغازین Ember data
آدرس درخواستی http://localhost:25918/posts به این معنا است که کلیه درخواستها، به همان آدرس و پورت ریشهی اصلی سایت ارسال میشوند. اما اگر یک ASP.NET Web API Controller را تعریف کنیم، نیاز است این درخواستها، برای مثال به آدرس api/posts ارسال شوند؛ بجای /posts.
برای این منظور پوشهی جدید Scripts\Adapters را ایجاد کرده و فایل web_api_adapter.js را با این محتوا به آن اضافه کنید:
DS.RESTAdapter.reopen({ namespace: 'api' });
<script src="Scripts/Adapters/web_api_adapter.js" type="text/javascript"></script>
تغییر تنظیمات پیش فرض ASP.NET Web API
در سمت سرور، بنابر اصول نامگذاری خواص، نامها با حروف بزرگ شروع میشوند:
namespace EmberJS03.Models { public class Post { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } } }
using System; using System.Web.Http; using System.Web.Routing; using Newtonsoft.Json.Serialization; namespace EmberJS03 { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { RouteTable.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); var settings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings; settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); } } }
نحوهی صحیح بازگشت اطلاعات از یک ASP.NET Web API جهت استفاده در Ember data
با تنظیمات فوق، اگر کنترلر جدیدی را به صورت ذیل جهت بازگشت لیست مطالب تهیه کنیم:
namespace EmberJS03.Controllers { public class PostsController : ApiController { public IEnumerable<Post> Get() { return DataSource.PostsList; } } }
WARNING: Encountered "0" in payload, but no model was found for model name "0" (resolved model name using DS.RESTSerializer.typeForRoot("0"))
http://jsonapi.codeplex.com
https://github.com/xqiu/MVCSPAWithEmberjs
https://github.com/rmichela/EmberDataAdapter
https://github.com/MilkyWayJoe/Ember-WebAPI-Adapter
http://blog.yodersolutions.com/using-ember-data-with-asp-net-web-api
http://emadibrahim.com/2014/04/09/emberjs-and-asp-net-web-api-and-json-serialization
و خلاصهی آنها به این صورت است:
خروجی JSON تولیدی توسط ASP.NET Web API چنین شکلی را دارد:
[ { Id: 1, Title: 'First Post' }, { Id: 2, Title: 'Second Post' } ]
{ posts: [{ id: 1, title: 'First Post' }, { id: 2, title: 'Second Post' }] }
using System.Web.Http; using EmberJS03.Models; namespace EmberJS03.Controllers { public class PostsController : ApiController { public object Get() { return new { posts = DataSource.PostsList }; } } }
اکنون اگر برنامه را اجرا کنید، در صفحهی اول آن، لیست عناوین مطالب را مشاهده خواهید کرد.
تاثیر قرارداد JSON API در حین ارسال اطلاعات به سرور توسط Ember data
در تکمیل کنترلرهای Web API مورد نیاز (کنترلرهای مطالب و نظرات)، نیاز به متدهای Post، Update و Delete هم خواهد بود. دقیقا فرامین ارسالی توسط Ember data توسط همین HTTP Verbs به سمت سرور ارسال میشوند. در این حالت اگر متد Post کنترلر نظرات را به این شکل طراحی کنیم:
public HttpResponseMessage Post(Comment comment)
{"comment":{"text":"data...","post":"3"}}
برای پردازش آن، یا باید از راه حلهای ثالث مطرح شده در ابتدای بحث استفاده کنید و یا میتوان مطابق کدهای ذیل، کل اطلاعات JSON ارسالی را توسط کتابخانهی JSON.NET نیز پردازش کرد:
namespace EmberJS03.Controllers { public class CommentsController : ApiController { public HttpResponseMessage Post(HttpRequestMessage requestMessage) { var jsonContent = requestMessage.Content.ReadAsStringAsync().Result; // {"comment":{"text":"data...","post":"3"}} var jObj = JObject.Parse(jsonContent); var comment = jObj.SelectToken("comment", false).ToObject<Comment>(); var id = 1; var lastItem = DataSource.CommentsList.LastOrDefault(); if (lastItem != null) { id = lastItem.Id + 1; } comment.Id = id; DataSource.CommentsList.Add(comment); // ارسال آی دی با فرمت خاص مهم است return Request.CreateResponse(HttpStatusCode.Created, new { comment = comment }); } } }
همچنین فرمت return نهایی هم مهم است. در این حالت خروجی ارسالی به سمت کاربر، باید مجددا با فرمت JSON API باشد؛ یعنی باید comment اصلاح شده را به همراه ریشهی comment ارسال کرد. در اینجا نیز anonymous object تهیه شده، چنین کاری را انجام میدهد.
Lazy loading در Ember data
تا اینجا اگر برنامه را اجرا کنید، لیست مطالب صفحهی اول را مشاهده خواهید کرد، اما لیست نظرات آنها را خیر؛ از این جهت که ضرورتی نداشت تا در بار اول ارسال لیست مطالب به سمت کاربر، تمام نظرات متناظر با آنها را هم ارسال کرد. بهتر است زمانیکه کاربر یک مطلب خاص را مشاهده میکند، نظرات خاص آنرا به سمت کاربر ارسال کنیم.
در تعاریف سمت کاربر Ember data، پارامتر دوم رابطهی hasMany که با async:true مشخص شدهاست، دقیقا معنای lazy loading را دارد.
Blogger.Post = DS.Model.extend({ title: DS.attr(), body: DS.attr(), comments: DS.hasMany('comment', { async: true } /* lazy loading */) });
الف) Idهای نظرات هر مطلب را به صورت یک آرایه، در بار اول ارسال لیست نظرات به سمت کاربر، تهیه و ارسال کنیم:
namespace EmberJS03.Models { public class Post { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } // lazy loading via an array of IDs public int[] Comments { set; get; } } }
ب) این روش به علت تعداد رفت و برگشت بیش از حد به سرور، کارآیی آنچنانی ندارد. بهتر است جهت مشاهدهی جزئیات یک مطلب، تنها یکبار درخواست Get کلیه نظرات آن صادر شود.
برای اینکار باید مدل برنامه را به شکل زیر تغییر دهیم:
namespace EmberJS03.Models { public class Post { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } // load related models via URLs instead of an array of IDs // ref. https://github.com/emberjs/data/pull/1371 public object Links { set; get; } public Post() { Links = new { comments = "comments" }; // api/posts/id/comments } } }
namespace EmberJS03 { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { RouteTable.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}/{name}", defaults: new { id = RouteParameter.Optional, name = RouteParameter.Optional } ); var settings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings; settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); } } }
در این حالت در طی یک درخواست میتوان کلیه نظرات را به سمت کاربر ارسال کرد. در اینجا نیز ذکر ریشهی comments همانند ریشه posts، الزامی است:
namespace EmberJS03.Controllers { public class PostsController : ApiController { //GET api/posts/id public object Get(int id) { return new { posts = DataSource.PostsList.FirstOrDefault(post => post.Id == id), comments = DataSource.CommentsList.Where(comment => comment.Post == id).ToList() }; } } }
پردازشهای async و متد transitionToRoute در Ember.js
اگر متد حذف مطالب را نیز به کنترلر Posts اضافه کنیم:
namespace EmberJS03.Controllers { public class PostsController : ApiController { public HttpResponseMessage Delete(int id) { var item = DataSource.PostsList.FirstOrDefault(x => x.Id == id); if (item == null) return Request.CreateResponse(HttpStatusCode.NotFound); DataSource.PostsList.Remove(item); //حذف کامنتهای مرتبط var relatedComments = DataSource.CommentsList.Where(comment => comment.Post == id).ToList(); relatedComments.ForEach(comment => DataSource.CommentsList.Remove(comment)); return Request.CreateResponse(HttpStatusCode.OK, new { post = item }); } } }
Attempted to handle event `pushedData` on while in state root.deleted.inFlight.
Blogger.PostController = Ember.ObjectController.extend({ isEditing: false, actions: { edit: function () { this.set('isEditing', true); }, save: function () { var post = this.get('model'); post.save(); this.set('isEditing', false); }, delete: function () { if (confirm('Do you want to delete this post?')) { var thisController = this; var post = this.get('model'); post.destroyRecord().then(function () { thisController.transitionToRoute('posts'); }); } } } });
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_05.zip
یکی از قابلیتهای جالب NHibernate امکان تعریف فیلدها به صورت پویا هستند. به این معنا که زیرساخت طراحی یک برنامه "فرم ساز" هم اکنون در اختیار شما است! سیستمی که امکان افزودن فیلدهای سفارشی را دارا است که توسط برنامه نویس در زمان طراحی اولیه آن ایجاد نشدهاند. در ادامه نحوهی تعریف و استفاده از این قابلیت را توسط Fluent NHibernate بررسی خواهیم کرد.
در اینجا کلاسی که قرار است توانایی افزودن فیلدهای سفارشی را داشته باشد به صورت زیر تعریف میشود:
using System.Collections;
namespace TestModel
{
public class DynamicEntity
{
public virtual int Id { get; set; }
public virtual IDictionary Attributes { set; get; }
}
}
Attributes در عمل همان فیلدهای سفارشی مورد نظر خواهند بود. جهت معرفی صحیح این قابلیت نیاز است تا نگاشت آنرا از نوع dynamic component تعریف کنیم:
using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;
namespace TestModel
{
public class DynamicEntityMapping : IAutoMappingOverride<DynamicEntity>
{
public void Override(AutoMapping<DynamicEntity> mapping)
{
mapping.Table("tblDynamicEntity");
mapping.Id(x => x.Id);
mapping.IgnoreProperty(x => x.Attributes);
mapping.DynamicComponent(x => x.Attributes,
c =>
{
c.Map(x => (string)x["field1"]);
c.Map(x => (string)x["field2"]).Length(300);
c.Map(x => (int)x["field3"]);
c.Map(x => (double)x["field4"]);
});
}
}
}
ابتدا از IgnoreProperty جهت ندید گرفتن Attributes استفاده کردیم. زیرا درغیراینصورت در حالت Auto mapping ، یک رابطه چند به یک به علت وجود IDictionary به صورت خودکار ایجاد خواهد شد که نیازی به آن نیست (یافتن این نکته نصف روز کار برد ....! چون مرتبا خطای An association from the table DynamicEntity refers to an unmapped class: System.Collections.Idictionary ظاهر میشد و مشخص نبود که مشکل از کجاست).
سپس Attributes به عنوان یک DynamicComponent معرفی شده است. در اینجا چهار فیلد سفارشی را اضافه کردهایم. به این معنا که اگر نیاز باشد تا فیلد سفارشی دیگری به سیستم اضافه شود باید یکبار Session factory ساخته شود و SchemaUpdate فراخوانی گردد و خوشبختانه با وجود Fluent NHibernate ، تمام این تغییرات در کدهای برنامه قابل انجام است (بدون نیاز به سر و کار داشتن با فایلهای XML نگاشتها و ویرایش دستی آنها). از تغییر نام جدول که برای مثال در اینجا tblDynamicEntity در نظر گرفته شده تا افزودن فیلدهای دیگر در قسمت DynamicComponent فوق.
همچنین باتوجه به اینکه این نوع تغییرات (ساخت دوبار سشن فکتوری) مواردی نیستند که قرار باشد هر ساعت انجام شوند، بنابراین سربار آنچنانی را به سیستم تحمیل نمیکنند.
drop table tblDynamicEntity
create table tblDynamicEntity (
Id INT IDENTITY NOT NULL,
field1 NVARCHAR(255) null,
field2 NVARCHAR(300) null,
field3 INT null,
field4 FLOAT null,
primary key (Id)
)
اگر با SchemaExport، اسکریپت خروجی معادل با نگاشت فوق را تهیه کنیم به جدول فوق خواهیم رسید. نوع و طول این فیلدهای سفارشی بر اساس نوعی که برای اشیاء دیکشنری مشخص میکنید، تعیین خواهند شد.
چند مثال جهت کار با این فیلدهای سفارشی یا پویا :
نحوهی افزودن رکوردهای جدید بر اساس خاصیتهای سفارشی:
//insert
object savedId = 0;
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var obj = new DynamicEntity();
obj.Attributes = new Hashtable();
obj.Attributes["field1"] = "test1";
obj.Attributes["field2"] = "test2";
obj.Attributes["field3"] = 1;
obj.Attributes["field4"] = 1.1;
savedId = session.Save(obj);
tx.Commit();
}
}
با خروجی
INSERT
INTO
tblDynamicEntity
(field1, field2, field3, field4)
VALUES
(@p0, @p1, @p2, @p3);
@p0 = 'test1' [Type: String (0)], @p1 = 'test2' [Type: String (0)], @p2 = 1
[Type: Int32 (0)], @p3 = 1.1 [Type: Double (0)]
نحوهی کوئری گرفتن از این اطلاعات (فعلا پایدارترین روشی را که برای آن یافتهام استفاده از HQL میباشد ...):
//query
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
//using HQL
var list = session
.CreateQuery("from DynamicEntity d where d.Attributes.field1=:p0")
.SetString("p0", "test1")
.List<DynamicEntity>();
if (list != null && list.Any())
{
Console.WriteLine(list[0].Attributes["field2"]);
}
tx.Commit();
}
}
select
dynamicent0_.Id as Id1_,
dynamicent0_.field1 as field2_1_,
dynamicent0_.field2 as field3_1_,
dynamicent0_.field3 as field4_1_,
dynamicent0_.field4 as field5_1_
from
tblDynamicEntity dynamicent0_
where
dynamicent0_.field1=@p0;
@p0 = 'test1' [Type: String (0)]
استفاده از HQL هم یک مزیت مهم دارد: چون به صورت رشته قابل تعریف است، به سادگی میتوان آنرا داخل دیتابیس ذخیره کرد. برای مثال یک سیستم گزارش ساز پویا هم در این کنار طراحی کرد ....
نحوهی به روز رسانی و حذف اطلاعات بر اساس فیلدهای پویا:
//update
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var entity = session.Get<DynamicEntity>(savedId);
if (entity != null)
{
entity.Attributes["field2"] = "new-val";
tx.Commit(); // Persist modification
}
}
}
//delete
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var entity = session.Get<DynamicEntity>(savedId);
if (entity != null)
{
session.Delete(entity);
tx.Commit();
}
}
}
EF Code First #2
در بخشی گفتید که
[Column("MyTableKey")] public int Id { set; get; }
export class Book { constructor( public id, public title:string, public pages:Array ){} } return this._http.get('getBook/1') .map(function(res){ var data = res.json(); return new Book(data.id, data.title, data.pages); })
به این صورت مدلهای خود را تعریف کرده و طبق مقالهی قبلی، Controllerهای هر یک را پیاده سازی نمایید:
public class Supplier { [Key] public string Key {get; set; } public string Name { get; set; } } public class Category { public int ID { get; set; } public string Name { get; set; } public virtual ICollection<Product> Products { get; set; } } public class Product { public int ID { get; set; } public string Name { get; set; } public decimal Price { get; set; } [ForeignKey("Category")] public int CategoryId { get; set; } public Category Category { get; set; } [ForeignKey("Supplier")] public string SupplierId { get; set; } public virtual Supplier Supplier { get; set; } }
پکیج Microsoft.AspNet.OData به تازگی ورژن 6 آن به صورت رسمی منتشر شده و شامل تغییراتی نسبت به نسخهی قبلی آن است. اولین نکتهی حائز اهمیت، Config آن است که به صورت زیر تغییر کرده و باید Optionهای مورد نیاز، کانفیگ شوند. در این نسخه DI نیز به Odata اضافه شده است:
public static void Register(HttpConfiguration config) { ODataModelBuilder odataModelBuilder = new ODataConventionModelBuilder(); var product = odataModelBuilder.EntitySet<Product>("Products"); var category = odataModelBuilder.EntitySet<Category>("Categories"); var supplier = odataModelBuilder.EntitySet<Supplier>("Suppliers"); var edmModel = odataModelBuilder.GetEdmModel(); supplier.EntityType.Ignore(c => c.Name); config.Select(System.Web.OData.Query.QueryOptionSetting.Allowed); config.MaxTop(25); config.OrderBy(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Expand(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Filter(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed); //config.MapODataServiceRoute("ODataRoute", "odata", edmModel); // کانفیگ به صورت معمولی config.MapODataServiceRoute("ODataRoute", "odata", builder => { builder.AddService(ServiceLifetime.Singleton, sp => edmModel); builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault()); }); }
[EnableQuery] public IQueryable<Product> Get() { return new List<Product> { new Product { Id = 1, Name = "name 1", Price = 11, Category = new Category {Id =1, Name = "Cat1" } }, new Product { Id = 2, Name = "name 2", Price = 12, Category = new Category {Id =2, Name = "Cat2" } }, new Product { Id = 3, Name = "name 3", Price = 13, Category = new Category {Id =3, Name = "Cat3" } }, new Product { Id = 4, Name = "name 4", Price = 14, Category = new Category {Id =4, Name = "Cat4" } }, }.AsQueryable(); ; }
Description | Option |
بسط دادن موجودیت مرتبط | $expand |
فیلتر کردن نتیجه، بر اساس شرطهای Boolean ی | $filter |
فرمان به سرور که تعداد رکوردهای بازگشتی را نیز نمایش دهد(مناسب برای پیاده سازی server-side pagging ) | $count |
مرتب کردن نتیجهی بازگشتی | $orderby |
select زدن روی پراپرتیهای درخواستی | $select |
پرش کردن از اولین رکورد به اندازهی n عدد | $skip |
فقط بازگرداندن n رکورد اول | $top |
/odata/Products?$select=Id,Name
/odata/Products?$top=3&$skip=2
/odata/Products?$count=true
{@odata.context: "http://localhost:4516/odata/$metadata#Products", @odata.count: 4,…} @odata.context:"http://localhost:4516/odata/$metadata#Products" @odata.count:4 value:[{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0},…] 0:{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0} 1:{Id: 2, Name: "name 2", Price: 12, SupplierId: 0, CategoryId: 0} 2:{Id: 3, Name: "name 3", Price: 13, SupplierId: 0, CategoryId: 0} 3:{Id: 4, Name: "name 4", Price: 14, SupplierId: 0, CategoryId: 0}
/odata/Products?$filter=Id eq 1
/odata/Products?$filter=Id gt 1 and Id lt 3
/odata/Products?$orderby=Id desc
/odata/Products?$expand=Category
/odata/Products?$expand=Category,Supplier
/odata/Categories(1)?$expand=Products/Supplier
[EnableQuery(PageSize = 10)]
محدود کردن Query Options
[EnableQuery (AllowedQueryOptions= AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]
[EnableQuery(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]
var product = odataModelBuilder.EntitySet<Product>("Products"); product.EntityType.Ignore(e => e.Price);
[EnableQuery(AllowedOrderByProperties = "Id,Name")]
public System.Web.Http.IHttpActionResult GetName(int key) { Product product = Get().Single(c => c.Id == key); return Ok(product.Name); }
/odata/Products(1)/Name/$value
HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 DataServiceVersion: 3.0 Content-Length: 3 Ali
Attribute Convention هایی هم برای اعتبارسنجی پراپرتیها موجود است که نام آنها واضح تعریف شدهاند:
Description | Attribute |
اجازهی فیلتر زدن بر روی آن پراپرتی داده نخواهد شد | NotFilterable |
اجازهی مرتب کردن بر روی آن پراپرتی داده نخواهد شد | NotSortable |
اجازهی select زدن بر روی آن پراپرتی داده نمیشود | NotNavigable |
اجازهی شمارش دهی بر روی آن Collection داده نمیشود | NotCountable |
اجازهی بسط دادن آن Collection داده نمیشود | NotExpandable |
و همچنین [AutoExpand] به صورت اتوماتیک آن موجودیت مورد نظر را بسط میدهد.
بطور مثال کدهای زیر را در مدل خود میتوانید مشاهده نمائید:
public class Product { public int Id { get; set; } public string Name { get; set; } [NotFilterable, NotSortable] public decimal Price { get; set; } [ForeignKey(nameof(SupplierId))] [NotNavigable] public virtual Supplier Supplier { get; set; } public int SupplierId { get; set; } [ForeignKey(nameof(CategoryId))] public virtual Category Category { get; set; } public int CategoryId { get; set; } [NotExpandable] public virtual ICollection<TestEntity> TestEnities { get; set; } }
فرض کنید پراپرتی زیر را به مدل خود اضافه کرده اید
public DateTimeOffset CreatedOn { get; set; }
حال کوئری زیر را برای فیلتر زدن، بر روی آن در اختیار داریم:
/odata/Products?$filter=year(CreatedOn) eq 2016
در اینجا فقط Product هایی بازگردانده میشوند که در سال 2016 ثبت شدهاند:
/odata/Products?$filter=CreatedOn lt cast(2017-04-01T04:11:31%2B08:00,Edm.DateTimeOffset)
کوئری فوق تاریخ مورد نظر را Cast کرده و همهی Product هایی را که قبل از این تاریخ ثبت شدهاند، باز میگرداند.
Nested Filter In Expand
/odata/Categories?$expand=Products($filter=Id gt 1 and Id lt 5)
همهی Categoryها به علاوه بسط دادن Product هایشان، در صورتیکه Id آنها بیشتر از 1 باشد
و یا حتی بر روی موجودیت بسط داده شده، select زده شود:
/odata/Categories?$expand=Products($select=Id,Name)
Custom Attribute
ضمنا به سادگی میتوان اتریبیوت سفارشی نوشت:
public class MyEnableQueryAttribute : EnableQueryAttribute { public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions) { // Don't apply Skip and Top. var ignoreQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Top; return queryOptions.ApplyTo(queryable, ignoreQueryOptions); } }
روی هر متدی از کنترلر خود که اتریبیوت [MyEnableQuery] را قرار دهید، دیگر قابلیت Skip, Top را نخواهد داشت.
Dependency Injection در آخرین نسخهی OData اضافه شده است. بطور پیشفرض OData بصورت case-sensitive رفتار میکند. برای تغییر دادن آن در نسخههای قدیمی، Extension Methodی به نام EnableCaseSensitive وجود داشت. اما در نسخهی جدید شما میتوانید پیاده سازی خاص خود را از هر کدام از بخشهای OData داشته باشید و با استفاده از تزریق وابستگی، آن را به config برنامهی خود اضافه کنید؛ برای مثال:
public class CaseInsensitiveResolver : ODataUriResolver { private bool _enableCaseInsensitive; public override bool EnableCaseInsensitive { get { return true; } set { _enableCaseInsensitive = value; } } }
اینجا پیاده سازی از ODataUriResolver انجام شده و متد EnableCaseInsensitive به صورت جدیدی override و در حالت default مقدار true را برمیگرداند.
حال به صورت زیر آن را میتوان به وابستگیهای config برنامه، اضافه نمود:
config.MapODataServiceRoute("ODataRoute", "odata", builder => { builder.AddService(ServiceLifetime.Singleton, sp => edmModel); builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault()); builder.AddService<ODataUriResolver>(ServiceLifetime.Singleton, sp => new CaseInsensitiveResolver()); // how enable case sensitive });
در قسمت بعدی به Actionها و Functionها در OData میپردازیم.