var serviceCollection = new ServiceCollection(); serviceCollection.AddDataProtection(); var services = serviceCollection.BuildServiceProvider(); var protectionProvider = ActivatorUtilities.CreateInstance<ProtectionProvider>(services); string result = protectionProvider.Encrypt(Str);
سیستم data protection به همراه اینترفیسی است به نام IXmlRepository که از آن میتوان برای مشخص سازی محل ذخیره سازی XML ایی اطلاعات کلید تولید شده استفاده کرد. این امکان هم وجود دارد که این اینترفیس را طوری پیاده سازی کرد تا اطلاعات را درون بانک اطلاعاتی ذخیره کند. به صورت ذیل:
ابتدا کلاس AppDataProtectionKey را به عنوان یک موجودیت جدید به سیستم EF معرفی میکنیم:
public class AppDataProtectionKey { public int Id { get; set; } public string FriendlyName { get; set; } public string XmlData { get; set; } }
سپس آنرا به Context برنامه به صورت ذیل اضافه میکنیم:
public virtual DbSet<AppDataProtectionKey> AppDataProtectionKeys { get; set; }
modelBuilder.Entity<AppDataProtectionKey>(builder => { builder.ToTable("AppDataProtectionKeys"); builder.HasIndex(e => e.FriendlyName).IsUnique(); });
در ادامه پیاده سازی ویژهی ذیل را از IXmlRepository، که از اطلاعات فوق استفاده میکند، تهیه خواهیم کرد:
public class DataProtectionKeyService : IXmlRepository { private readonly IServiceProvider _serviceProvider; public DataProtectionKeyService(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; _serviceProvider.CheckArgumentIsNull(nameof(_serviceProvider)); } public IReadOnlyCollection<XElement> GetAllElements() { return _serviceProvider.RunScopedContext<ReadOnlyCollection<XElement>>(context => { var dataProtectionKeys = context.Set<AppDataProtectionKey>(); return new ReadOnlyCollection<XElement>(dataProtectionKeys.Select(k => XElement.Parse(k.XmlData)).ToList()); }); } public void StoreElement(XElement element, string friendlyName) { // We need a separate context to call its SaveChanges several times, // without using the current request's context and changing its internal state. _serviceProvider.RunScopedContext(context => { var dataProtectionKeys = context.Set<AppDataProtectionKey>(); var entity = dataProtectionKeys.SingleOrDefault(k => k.FriendlyName == friendlyName); if (null != entity) { entity.XmlData = element.ToString(); dataProtectionKeys.Update(entity); } else { dataProtectionKeys.Add(new AppDataProtectionKey { FriendlyName = friendlyName, XmlData = element.ToString() }); } context.SaveChanges(); }); } }
اطلاعات متدهای سرویس فوق به صورت خودکار توسط سیستم data-protection تامین میشوند. تنها کاری را که در اینجا انجام دادهایم، گوش فرادادن به این تغییرات و ذخیره سازی آنها در بانک اطلاعاتی است.
مرحلهی آخر کار، معرفی این تغییرات به سیستم است که نحوهی انجام آنرا در ذیل مشاهده میکنید:
private static void addCustomDataProtection(this IServiceCollection services, SiteSettings siteSettings) { services.AddScoped<IXmlRepository, DataProtectionKeyService>(); services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(serviceProvider => { return new ConfigureOptions<KeyManagementOptions>(options => { var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { options.XmlRepository = scope.ServiceProvider.GetService<IXmlRepository>(); } }); }); services .AddDataProtection() .SetDefaultKeyLifetime(siteSettings.CookieOptions.ExpireTimeSpan) .SetApplicationName(siteSettings.CookieOptions.CookieName) .UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration { EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC, ValidationAlgorithm = ValidationAlgorithm.HMACSHA256 }); }
همین مقدار تنظیم سبب خواهد شد تا به صورت خودکار اطلاعات موقتی کلیدهای رمزنگاری سیستم data-protection در بانک اطلاعاتی ذخیره شده و یا بازیابی شوند.
این تغییرات به پروژهی DNTIdentity اعمال شدهاند.
یک اپلیکیشن با SQL Membership بسازید
حال با استفاده از ابزار ASP.NET Configuration دو کاربر جدید بسازید: oldAdminUser و oldUser.
نقش جدیدی با نام Admin بسازید و کاربر oldAdminUser را به آن اضافه کنید.
بخش جدیدی با نام Admin در سایت خود بسازید و فرمی بنام Default.aspx به آن اضافه کنید. همچنین فایل web.config این قسمت را طوری پیکربندی کنید تا تنها کاربرانی که در نقش Admin هستند به آن دسترسی داشته باشند. برای اطلاعات بیشتر به این لینک مراجعه کنید.
پنجره Server Explorer را باز کنید و جداول ساخته شده توسط SQL Membership را بررسی کنید. اطلاعات اصلی کاربران که برای ورود به سایت استفاده میشوند، در جداول aspnet_Users و aspnet_Membership ذخیره میشوند. دادههای مربوط به نقشها نیز در جدول aspnet_Roles ذخیره خواهند شد. رابطه بین کاربران و نقشها نیز در جدول aspnet_UsersInRoles ذخیره میشود، یعنی اینکه هر کاربری به چه نقش هایی تعلق دارد.
برای مدیریت اساسی سیستم عضویت، مهاجرت جداول ذکر شده به سیستم جدید ASP.NET Identity کفایت میکند.
مهاجرت به Visual Studio 2013
- برای شروع ابتدا Visual Studio Express 2013 for Web یا Visual Studio 2013 را نصب کنید.
- حال پروژه ایجاد شده را در نسخه جدید ویژوال استودیو باز کنید. اگر نسخه ای از SQL Server Express را روی سیستم خود نصب نکرده باشید، هنگام باز کردن پروژه پیغامی به شما نشان داده میشود. دلیل آن وجود رشته اتصالی است که از SQL Server Express استفاده میکند. برای رفع این مساله میتوانید SQL Express را نصب کنید، و یا رشته اتصال را طوری تغییر دهید که از LocalDB استفاده کند.
- فایل web.config را باز کرده و رشته اتصال را مانند تصویر زیر ویرایش کنید.
- پنجره Server Explorer را باز کنید و مطمئن شوید که الگوی جداول و دادهها قابل رویت هستند.
- سیستم ASP.NET Identity با نسخه 4.5 دات نت فریم ورک و بالاتر سازگار است. پس نسخه فریم ورک پروژه را به آخرین نسخه (4.5.1) تغییر دهید.
پروژه را Build کنید تا مطمئن شوید هیچ خطایی وجود ندارد.
نصب پکیجهای NuGet
- Microsoft.AspNet.Identity.Owin
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.Facebook
- Microsoft.Owin.Security.Google
- Microsoft.Owin.Security.MicrosoftAccount
- Microsoft.Owin.Security.Twitter
مهاجرت دیتابیس فعلی به سیستم ASP.NET Identity
در پنجره کوئری باز شده، تمام محتویات فایل Migrations.sql را کپی کنید. سپس اسکریپت را با کلیک کردن دکمه Execute اجرا کنید.
ممکن است با اخطاری مواجه شوید مبنی بر آنکه امکان حذف (drop) بعضی از جداول وجود نداشت. دلیلش آن است که چهار عبارت اولیه در این اسکریپت، تمام جداول مربوط به Identity را در صورت وجود حذف میکنند. از آنجا که با اجرای اولیه این اسکریپت چنین جداولی وجود ندارند، میتوانیم این خطاها را نادیده بگیریم. حال پنجره Server Explorer را تازه (refresh) کنید و خواهید دید که پنج جدول جدید ساخته شده اند.
لیست زیر نحوه Map کردن اطلاعات از جداول SQL Membership به سیستم Identity را نشان میدهد.
- aspnet_Roles --> AspNetRoles
- aspnet_Users, aspnet_Membership --> AspNetUsers
- aspnet_UsersInRoles --> AspNetUserRoles
ساختن مدلها و صفحات عضویت
کلاس User باید کلاس IdentityUser را که در اسمبلی Microsoft.AspNet.Identity.EntityFramework وجود دارد گسترش دهد. خاصیت هایی را تعریف کنید که نماینده الگوی جدول AspNetUser هستند. خواص ID, Username, PasswordHash و SecurityStamp در کلاس IdentityUser تعریف شده اند، بنابراین این خواص را در لیست زیر نمیبینید.
public class User : IdentityUser { public User() { CreateDate = DateTime.Now; IsApproved = false; LastLoginDate = DateTime.Now; LastActivityDate = DateTime.Now; LastPasswordChangedDate = DateTime.Now; LastLockoutDate = DateTime.Parse("1/1/1754"); FailedPasswordAnswerAttemptWindowStart = DateTime.Parse("1/1/1754"); FailedPasswordAttemptWindowStart = DateTime.Parse("1/1/1754"); } public System.Guid ApplicationId { get; set; } public string MobileAlias { get; set; } public bool IsAnonymous { get; set; } public System.DateTime LastActivityDate { get; set; } public string MobilePIN { get; set; } public string Email { get; set; } public string LoweredEmail { get; set; } public string LoweredUserName { 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; } }
حال برای دسترسی به دیتابیس مورد نظر، نیاز به یک DbContext داریم. اسمبلی Microsoft.AspNet.Identity.EntityFramework کلاسی با نام IdentityDbContext دارد که پیاده سازی پیش فرض برای دسترسی به دیتابیس ASP.NET Identity است. نکته قابل توجه این است که IdentityDbContext آبجکتی از نوع TUser را میپذیرد. TUser میتواند هر کلاسی باشد که از IdentityUser ارث بری کرده و آن را گسترش میدهد.
در پوشه Models کلاس جدیدی با نام ApplicationDbContext بسازید که از IdentityDbContext ارث بری کرده و از کلاس User استفاده میکند.
public class ApplicationDbContext : IdentityDbContext<User> { }
مدیریت کاربران در ASP.NET Identity توسط کلاسی با نام UserManager انجام میشود که در اسمبلی Microsoft.AspNet.Identity.EntityFramework قرار دارد. چیزی که ما در این مرحله نیاز داریم، کلاسی است که از UserManager ارث بری میکند و آن را طوری توسعه میدهد که از کلاس User استفاده کند.
در پوشه Models کلاس جدیدی با نام UserManager بسازید.
public class UserManager : UserManager<User> { }
کلمه عبور کاربران بصورت رمز نگاری شده در دیتابیس ذخیره میشوند. الگوریتم رمز نگاری SQL Membership با سیستم ASP.NET Identity تفاوت دارد. هنگامی که کاربران قدیمی به سایت وارد میشوند، کلمه عبورشان را توسط الگوریتمهای قدیمی SQL Membership رمزگشایی میکنیم، اما کاربران جدید از الگوریتمهای ASP.NET Identity استفاده خواهند کرد.
کلاس UserManager خاصیتی با نام PasswordHasher دارد. این خاصیت نمونه ای از یک کلاس را ذخیره میکند، که اینترفیس IPasswordHasher را پیاده سازی کرده است. این کلاس هنگام تراکنشهای احراز هویت کاربران استفاده میشود تا کلمههای عبور را رمزنگاری/رمزگشایی شوند. در کلاس UserManager کلاس جدیدی بنام SQLPasswordHasher بسازید. کد کامل را در لیست زیر مشاهده میکنید.
public class SQLPasswordHasher : PasswordHasher { public override string HashPassword(string password) { return base.HashPassword(password); } public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) { string[] passwordProperties = hashedPassword.Split('|'); if (passwordProperties.Length != 3) { return base.VerifyHashedPassword(hashedPassword, providedPassword); } else { string passwordHash = passwordProperties[0]; int passwordformat = 1; string salt = passwordProperties[2]; if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase)) { return PasswordVerificationResult.SuccessRehashNeeded; } else { return PasswordVerificationResult.Failed; } } } //This is copied from the existing SQL providers and is provided only for back-compat. private string EncryptPassword(string pass, int passwordFormat, string salt) { if (passwordFormat == 0) // MembershipPasswordFormat.Clear return pass; byte[] bIn = Encoding.Unicode.GetBytes(pass); byte[] bSalt = Convert.FromBase64String(salt); byte[] bRet = null; if (passwordFormat == 1) { // MembershipPasswordFormat.Hashed HashAlgorithm hm = HashAlgorithm.Create("SHA1"); if (hm is KeyedHashAlgorithm) { KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm; if (kha.Key.Length == bSalt.Length) { kha.Key = bSalt; } else if (kha.Key.Length < bSalt.Length) { byte[] bKey = new byte[kha.Key.Length]; Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length); kha.Key = bKey; } else { byte[] bKey = new byte[kha.Key.Length]; for (int iter = 0; iter < bKey.Length; ) { int len = Math.Min(bSalt.Length, bKey.Length - iter); Buffer.BlockCopy(bSalt, 0, bKey, iter, len); iter += len; } kha.Key = bKey; } bRet = kha.ComputeHash(bIn); } else { byte[] bAll = new byte[bSalt.Length + bIn.Length]; Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length); Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length); bRet = hm.ComputeHash(bAll); } } return Convert.ToBase64String(bRet); } }
دقت کنید تا فضاهای نام System.Text و System.Security.Cryptography را وارد کرده باشید.
متد EncodePassword کلمه عبور را بر اساس پیاده سازی پیش فرض SQL Membership رمزنگاری میکند. این الگوریتم از System.Web گرفته میشود. اگر اپلیکیشن قدیمی شما از الگوریتم خاصی استفاده میکرده است، همینجا باید آن را منعکس کنید. دو متد دیگر نیز بنامهای HashPassword و VerifyHashedPassword نیاز داریم. این متدها از EncodePassword برای رمزنگاری کلمههای عبور و تایید آنها در دیتابیس استفاده میکنند.
سیستم SQL Membership برای رمزنگاری (Hash) کلمههای عبور هنگام ثبت نام و تغییر آنها توسط کاربران، از PasswordHash, PasswordSalt و PasswordFormat استفاده میکرد. در روند مهاجرت، این سه فیلد در ستون PasswordHash جدول AspNetUsers ذخیره شده و با کاراکتر '|' جدا شده اند. هنگام ورود کاربری به سایت، اگر کله عبور شامل این فیلدها باشد از الگوریتم SQL Membership برای بررسی آن استفاده میکنیم. در غیر اینصورت از پیاده سازی پیش فرض ASP.NET Identity استفاده خواهد شد. با این روش، کاربران قدیمی لازم نیست کلمههای عبور خود را صرفا بدلیل مهاجرت اپلیکیشن ما تغییر دهند.
کلاس UserManager را مانند قطعه کد زیر بروز رسانی کنید.
public UserManager() : base(new UserStore<User>(new ApplicationDbContext())) { this.PasswordHasher = new SQLPasswordHasher(); }
ایجاد صفحات جدید مدیریت کاربران
- فایلهای Register.aspx.cs و Login.aspx.cs از کلاس UserManager استفاده میکنند. این ارجاعات را با کلاس UserManager جدیدی که در پوشه Models ساختید جایگزین کنید.
- همچنین ارجاعات استفاده از کلاس IdentityUser را به کلاس User که در پوشه Models ساختید تغییر دهید.
- لازم است توسعه دهنده مقدار ApplicationId را برای کاربران جدید طوری تنظیم کند که با شناسه اپلیکیشن جاری تطابق داشته باشد. برای این کار میتوانید پیش از ساختن حسابهای کاربری جدید در فایل Register.aspx.cs ابتدا شناسه اپلیکیشن را بدست آورید و اطلاعات کاربر را بدرستی تنظیم کنید.
private Guid GetApplicationID() { using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["ApplicationServices"].ConnectionString)) { string queryString = "SELECT ApplicationId from aspnet_Applications WHERE ApplicationName = '/'"; //Set application name as in database SqlCommand command = new SqlCommand(queryString, connection); command.Connection.Open(); var reader = command.ExecuteReader(); while (reader.Read()) { return reader.GetGuid(0); } return Guid.NewGuid(); } }
var currentApplicationId = GetApplicationID(); User user = new User() { UserName = Username.Text, ApplicationId=currentApplicationId, …};
نحوه ذخیره شدن متن در فایلهای PDF
حتما نیاز است پیشنیاز فوق را یکبار مطالعه کنید تا علت خروجیهای متفاوتی را که در ادامه ملاحظه خواهید نمود، بهتر مشخص شوند. همچنین فایل PDF ایی که مورد بررسی قرار خواهد گرفت، همان فایلی است که توسط متد writePdf ذکر شده در پیشنیاز تهیه شده است.
دو کلاس متفاوت برای استخراج متن از فایلهای PDF در iTextSharp وجود دارند:
الف) SimpleTextExtractionStrategy
using System.Diagnostics; using System.IO; using iTextSharp.text; using iTextSharp.text.pdf; using iTextSharp.text.pdf.parser; namespace TestReaders { class Program { private static void readPdf1() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { var text = PdfTextExtractor.GetTextFromPage(reader, i, new SimpleTextExtractionStrategy()); File.WriteAllText("page-" + i + "-text.txt", text); } reader.Close(); } static void Main(string[] args) { readPdf1(); } } }
Test ld Wor llo He Hello People
ب) LocationTextExtractionStrategy
همان مثال قبل را درنظر بگیرید، اینبار به شکل زیر:
private static void readPdf2() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy()); File.WriteAllText("page-" + i + "-text.txt", text); } reader.Close(); }
Test Hello World Hello People
استخراج متون فارسی از فایلهای PDF توسط iTextSharp
روشهای فوق با PDFهای فارسی هم کار میکنند اما خروجی حاصل آن مفهوم نیست و نیاز به پردازش ثانوی دارد. ابتدا مثال زیر را درنظر بگیرید:
static void writePdf2() { using (var document = new Document(PageSize.A4)) { var writer = PdfWriter.GetInstance(document, new FileStream("test.pdf", FileMode.Create)); document.Open(); FontFactory.Register("c:\\windows\\fonts\\tahoma.ttf"); var tahoma = FontFactory.GetFont("tahoma", BaseFont.IDENTITY_H); ColumnText.ShowTextAligned( canvas: writer.DirectContent, alignment: Element.ALIGN_CENTER, phrase: new Phrase("تست میشود", tahoma), x: 100, y: 100, rotation: 0, runDirection: PdfWriter.RUN_DIRECTION_RTL, arabicOptions: 0); } Process.Start("test.pdf"); }
ﺩﻮﺷﻲﻣ ﺖﺴﺗ
private static void readPdf2() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy()); text = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(text)); File.WriteAllText("page-" + i + "-text.txt", text, Encoding.UTF8); } reader.Close(); }
ﺩﻮﺷﻲﻣ ﺖﺴﺗ
using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Security; namespace TestReaders { [SuppressUnmanagedCodeSecurity] class GdiMethods { [DllImport("GDI32.dll")] public static extern bool DeleteObject(IntPtr hgdiobj); [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern uint GetCharacterPlacement(IntPtr hdc, string lpString, int nCount, int nMaxExtent, [In, Out] ref GcpResults lpResults, uint dwFlags); [DllImport("GDI32.dll")] public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj); } [StructLayout(LayoutKind.Sequential)] struct GcpResults { public uint lStructSize; [MarshalAs(UnmanagedType.LPTStr)] public string lpOutString; public IntPtr lpOrder; public IntPtr lpDx; public IntPtr lpCaretPos; public IntPtr lpClass; public IntPtr lpGlyphs; public uint nGlyphs; public int nMaxFit; } public class UnicodeCharacterPlacement { const int GcpReorder = 0x0002; GCHandle _caretPosHandle; GCHandle _classHandle; GCHandle _dxHandle; GCHandle _glyphsHandle; GCHandle _orderHandle; public Font Font { set; get; } public string Apply(string lines) { if (string.IsNullOrWhiteSpace(lines)) return string.Empty; return Apply(lines.Split('\n')).Aggregate((s1, s2) => s1 + s2); } public IEnumerable<string> Apply(IEnumerable<string> lines) { if (Font == null) throw new ArgumentNullException("Font is null."); if (!hasUnicodeText(lines)) return lines; var graphics = Graphics.FromHwnd(IntPtr.Zero); var hdc = graphics.GetHdc(); try { var font = (Font)Font.Clone(); var hFont = font.ToHfont(); var fontObject = GdiMethods.SelectObject(hdc, hFont); try { var results = new List<string>(); foreach (var line in lines) results.Add(modifyCharactersPlacement(line, hdc)); return results; } finally { GdiMethods.DeleteObject(fontObject); GdiMethods.DeleteObject(hFont); font.Dispose(); } } finally { graphics.ReleaseHdc(hdc); graphics.Dispose(); } } void freeResources() { _orderHandle.Free(); _dxHandle.Free(); _caretPosHandle.Free(); _classHandle.Free(); _glyphsHandle.Free(); } static bool hasUnicodeText(IEnumerable<string> lines) { return lines.Any(line => line.Any(chr => chr >= '\u00FF')); } void initializeResources(int textLength) { _orderHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned); _dxHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned); _caretPosHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned); _classHandle = GCHandle.Alloc(new byte[textLength], GCHandleType.Pinned); _glyphsHandle = GCHandle.Alloc(new short[textLength], GCHandleType.Pinned); } string modifyCharactersPlacement(string text, IntPtr hdc) { var textLength = text.Length; initializeResources(textLength); try { var gcpResult = new GcpResults { lStructSize = (uint)Marshal.SizeOf(typeof(GcpResults)), lpOutString = new String('\0', textLength), lpOrder = _orderHandle.AddrOfPinnedObject(), lpDx = _dxHandle.AddrOfPinnedObject(), lpCaretPos = _caretPosHandle.AddrOfPinnedObject(), lpClass = _classHandle.AddrOfPinnedObject(), lpGlyphs = _glyphsHandle.AddrOfPinnedObject(), nGlyphs = (uint)textLength, nMaxFit = 0 }; var result = GdiMethods.GetCharacterPlacement(hdc, text, textLength, 0, ref gcpResult, GcpReorder); return result != 0 ? gcpResult.lpOutString : text; } finally { freeResources(); } } } }
در اینجا برای اصلاح متد readPdf2 خواهیم داشت:
private static void readPdf2() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy()); text = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(text)); text = new UnicodeCharacterPlacement { Font = new System.Drawing.Font("Tahoma", 12) }.Apply(text); File.WriteAllText("page-" + i + "-text.txt", text, Encoding.UTF8); } reader.Close(); }
سؤال: آیا این روش با تمام PDFهای فارسی کار میکند؟
پاسخ: خیر! همانطور که در پیشنیاز مطلب جاری عنوان شد، در یک حالت خاص، PDF writer میتواند شماره Glyphها را کاملا عوض کرده و در فایل PDF نهایی ثبت کند. خروجی حاصل در برنامه Adobe reader خوانا است، چون نمایش را بر اساس اطلاعات هندسی Glyphها انجام میدهد؛ اما خروجی متنی آن به نوعی obfuscated است چون مثلا حرف A آن به کاراکتر مرسوم دیگری نگاشت شده است.
namespace BlazorServer.Services { public interface IHotelRoomService { Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO); Task<int> DeleteHotelRoomAsync(int roomId); IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync(); Task<HotelRoomDTO> GetHotelRoomAsync(int roomId); Task<HotelRoomDTO> IsRoomUniqueAsync(string name); Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO); } }
تعریف کامپوننتهای ابتدایی نمایش لیست اتاقها و ثبت و ویرایش آنها
در ابتدا کامپوننتهای خالی نمایش لیست اتاقها و همچنین فرم خالی ثبت و ویرایش آنها را به همراه مسیریابیهای مرتبط، ایجاد میکنیم. به همین جهت ابتدا داخل پوشهی Pages، پوشهی جدید HotelRoom را ایجاد کرده و فایل جدید HotelRoomList.razor را با محتوای ابتدایی زیر، به آن اضافه میکنیم.
@page "/hotel-room" <div class="row mt-4"> <div class="col-8"> <h4 class="card-title text-info">Hotel Rooms</h4> </div> <div class="col-3 offset-1"> <NavLink href="hotel-room/create" class="btn btn-info">Add New Room</NavLink> </div> </div> @code { }
<li class="nav-item px-3"> <NavLink class="nav-link" href="hotel-room"> <span class="oi oi-list-rich" aria-hidden="true"></span> Hotel Rooms </NavLink> </li>
تا اینجا صفحهی ابتدایی نمایش لیست اتاقها، به همراه یک دکمهی افزودن اتاق جدید نیز هست. به همین جهت فایل جدید Pages\HotelRoom\HotelRoomUpsert.razor را به همراه مسیریابی hotel-room/create/ برای تعریف کامپوننت ابتدایی ثبت و ویرایش اطلاعات اتاقها، اضافه میکنیم:
@page "/hotel-room/create" <h3>HotelRoomUpsert</h3> @code { }
- NavLink تعریف شدهی در کامپوننت نمایش لیست اتاقها، به مسیریابی کامپوننت فوق اشاره میکند.
ایجاد فرم ثبت یک اتاق جدید
برای ثبت یک اتاق جدید نیاز است به مدل UI آن که همان HotelRoomDTO تعریف شدهی در قسمت قبل است، دسترسی داشت. به همین جهت در پروژهی BlazorServer.App، ارجاعی را به پروژهی BlazorServer.Models.csproj اضافه میکنیم:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <ProjectReference Include="..\BlazorServer.Models\BlazorServer.Models.csproj" /> </ItemGroup> </Project>
@using BlazorServer.Models
@page "/hotel-room/create" <div class="row mt-2 mb-5"> <h3 class="card-title text-info mb-3 ml-3">@Title Hotel Room</h3> <div class="col-md-12"> <div class="card"> <div class="card-body"> <EditForm Model="HotelRoomModel"> <div class="form-group"> <label>Name</label> <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText> </div> </EditForm> </div> </div> </div> </div> @code { private HotelRoomDTO HotelRoomModel = new HotelRoomDTO(); private string Title = "Create"; }
- در برنامههای Blazor، کامپوننت ویژهی EditForm را بجای تگ استاندارد form، مورد استفاده قرار میدهیم.
- این کامپوننت، مدل فرم را از فیلد HotelRoomModel که در قسمت کدها تعریف کردیم، دریافت میکند. کار آن تامین اطلاعات فیلدهای فرم است.
- سپس در EditForm تعریف شده، بجای المان استاندارد input، از کامپوننت InputText برای دریافت اطلاعات متنی استفاده میشود. با bind-value@ در قسمت چهارم این سری بیشتر آشنا شدیم و کار آن two-way data binding است. در اینجا هر اطلاعاتی که وارد میشود، سبب به روز رسانی خودکار مقدار خاصیت HotelRoomModel.Name میشود و برعکس.
یک نکته: در قسمت قبل، مدل UI را از نوع رکورد C# 9.0 و init only تعریف کردیم. رکوردها، با EditForm و two-way databinding آن سازگاری ندارند (bind-value@ در اینجا) و بیشتر برای کنترلرهای برنامههای Web API که یکبار قرار است کار وهله سازی آنها در زمان دریافت اطلاعات از کاربر صورت گیرد، مناسب هستند و نه با فرمهای پویای Blazor. به همین جهت به پروژهی BlazorServer.Models مراجعه کرده و نوع آنها را به کلاس و initها را به set معمولی تغییر میدهیم تا در فرمهای Blazor هم قابل استفاده شوند.
تا اینجا کامپوننت ثبت اطلاعات یک اتاق جدید، چنین شکلی را پیدا کردهاست:
تکمیل سایر فیلدهای فرم ورود اطلاعات اتاق
پس از تعریف فیلد ورود اطلاعات نام اتاق، سایر فیلدهای متناظر با HotelRoomDTO را نیز به صورت زیر به EditForm تعریف شده اضافه میکنیم که در اینجا از InputNumber برای دریافت اطلاعات عددی و از InputTextArea، برای دریافت اطلاعات متنی چندسطری استفاده شدهاست:
<EditForm Model="HotelRoomModel"> <div class="form-group"> <label>Name</label> <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText> </div> <div class="form-group"> <label>Occupancy</label> <InputNumber @bind-Value="HotelRoomModel.Occupancy" class="form-control"></InputNumber> </div> <div class="form-group"> <label>Rate</label> <InputNumber @bind-Value="HotelRoomModel.RegularRate" class="form-control"></InputNumber> </div> <div class="form-group"> <label>Sq ft.</label> <InputText @bind-Value="HotelRoomModel.SqFt" class="form-control"></InputText> </div> <div class="form-group"> <label>Details</label> <InputTextArea @bind-Value="HotelRoomModel.Details" class="form-control"></InputTextArea> </div> <div class="form-group"> <button class="btn btn-primary">@Title Room</button> <NavLink href="hotel-room" class="btn btn-secondary">Back to Index</NavLink> </div> </EditForm>
تعریف اعتبارسنجیهای فیلدهای یک فرم Blazor
در حین تعریف یک فرم، برای واکنش نشان دادن به دکمهی submit، میتوان رویداد OnSubmit را به کامپوننت EditForm اضافه کرد که سبب فراخوانی متدی در قسمت کدهای کامپوننت جاری خواهد شد؛ مانند فراخوانی متد HandleHotelRoomUpsert در مثال زیر:
<EditForm Model="HotelRoomModel" OnSubmit="HandleHotelRoomUpsert"> </EditForm> @code { private HotelRoomDTO HotelRoomModel = new HotelRoomDTO(); private async Task HandleHotelRoomUpsert() { } }
اگر این مورد، مدنظر نیست، میتوان بجای OnSubmit، از رویداد OnValidSubmit استفاده کرد. در این حالت اگر اعتبارسنجی مدل فرم با شکست مواجه شود، دیگر متد HandleHotelRoomUpsert فراخوانی نخواهد شد. همچنین در این حالت میتوان خطاهای اعتبارسنجی را نیز در فرم نمایش داد:
<EditForm Model="HotelRoomModel" OnValidSubmit="HandleHotelRoomUpsert"> <DataAnnotationsValidator /> @*<ValidationSummary />*@ <div class="form-group"> <label>Name</label> <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText> <ValidationMessage For="()=>HotelRoomModel.Name"></ValidationMessage> </div> <div class="form-group"> <label>Occupancy</label> <InputNumber @bind-Value="HotelRoomModel.Occupancy" class="form-control"></InputNumber> <ValidationMessage For="()=>HotelRoomModel.Occupancy"></ValidationMessage> </div> <div class="form-group"> <label>Rate</label> <InputNumber @bind-Value="HotelRoomModel.RegularRate" class="form-control"></InputNumber> <ValidationMessage For="()=>HotelRoomModel.RegularRate"></ValidationMessage> </div>
- کامپوننت DataAnnotationsValidator، اعتبارسنجی مبتنی بر data annotations را مانند [Required]، در دامنهی دید یک EditForm فعال میکند.
- اگر خواستیم تمام خطاهای اعتبارسنجی را به صورت خلاصهای در بالای فرم نمایش دهیم، میتوان از کامپوننت ValidationSummary استفاده کرد.
- و یا اگر خواستیم خطاها را به صورت اختصاصیتری ذیل هر تکستباکس نمایش دهیم، میتوان از کامپوننت ValidationMessage کمک گرفت. خاصیت For آن از نوع <Expression<System.Func تعریف شدهاست که اجازهی تعریف strongly typed نام خاصیت در حال اعتبارسنجی را به صورتی که مشاهده میکنید، میسر میکند.
ثبت اولین اتاق هتل
در ادامه میخواهیم روال رویدادگردان HandleHotelRoomUpsert را مدیریت کنیم. به همین جهت نیاز به کار با سرویس IHotelRoomService ابتدای بحث خواهد بود. بنابراین در ابتدا به فایل BlazorServer.App\_Imports.razor مراجعه کرده و فضای نام سرویسهای برنامه را اضافه میکنیم:
@using BlazorServer.Services
@page "/hotel-room/create" @inject IHotelRoomService HotelRoomService @inject NavigationManager NavigationManager @code { private HotelRoomDTO HotelRoomModel = new HotelRoomDTO(); private string Title = "Create"; private async Task HandleHotelRoomUpsert() { var roomDetailsByName = await HotelRoomService.IsRoomUniqueAsync(HotelRoomModel.Name); if (roomDetailsByName != null) { //there is a duplicate room. show an error msg. return; } var createdResult = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel); NavigationManager.NavigateTo("hotel-room"); } }
اگر پیشتر با ASP.NET Web Forms کار کرده باشید (اولین روش توسعهی برنامههای وب در دنیای دات نت)، مدل برنامه نویسی Blazor Server، بسیار شبیه به کار با وب فرمها است؛ البته بر اساس آخرین تغییرات دنیای دانت نت مانند برنامه نویسی async، کار با سرویسها، تزریق وابستگیهای توکار و غیره.
نمایش لیست اتاقهای ثبت شده
تا اینجا موفق شدیم اطلاعات یک مدل اعتبارسنجی شده را در بانک اطلاعاتی ثبت کنیم. مرحلهی بعد، نمایش لیست اطلاعات ثبت شدهی در بانک اطلاعاتی است. بنابراین به کامپوننت HotelRoomList.razor مراجعه کرده و آنرا به صورت زیر تکمیل میکنیم:
@page "/hotel-room" @inject IHotelRoomService HotelRoomService <div class="row mt-4"> <div class="col-8"> <h4 class="card-title text-info">Hotel Rooms</h4> </div> <div class="col-3 offset-1"> <NavLink href="hotel-room/create" class="btn btn-info">Add New Room</NavLink> </div> </div> <div class="row mt-4"> <div class="col-12"> <table class="table table-bordered table-hover"> <thead> <tr> <th>Name</th> <th>Occupancy</th> <th>Rate</th> <th> Sqft </th> <th> </th> </tr> </thead> <tbody> @if (HotelRooms.Any()) { foreach (var room in HotelRooms) { <tr> <td>@room.Name</td> <td>@room.Occupancy</td> <td>@room.RegularRate.ToString("c")</td> <td>@room.SqFt</td> <td></td> </tr> } } else { <tr> <td colspan="5">No records found</td> </tr> } </tbody> </table> </div> </div> @code { private List<HotelRoomDTO> HotelRooms = new List<HotelRoomDTO>(); protected override async Task OnInitializedAsync() { await foreach(var room in HotelRoomService.GetAllHotelRoomsAsync()) { HotelRooms.Add(room); } } }
- متد GetAllHotelRoomsAsync، لیست اتاقهای ثبت شده را بازگشت میدهد. البته خروجی آن از نوع <IAsyncEnumerable<HotelRoomDTO است که از زمان C# 8.0 ارائه شد و روش کار با آن اندکی متفاوت است. IAsyncEnumerableها را باید توسط await foreach پردازش کرد.
- همانطور که در مطلب بررسی چرخهی حیات کامپوننتها نیز عنوان شد، متدهای رویدادگران OnInitialized و نمونهی async آن برای دریافت اطلاعات از سرویسها طراحی شدهاند که در اینجا نمونهای از آنرا مشاهده میکنید.
- پس از تشکیل لیست اتاقها، حلقهی foreach (var room in HotelRooms) تعریف شده، ردیفهای آنرا در UI نمایش میدهد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-14.zip
public class LockFilter : ActionFilterAttribute { static ConcurrentDictionary<StringBuilder, int> _properties; static LockFilter() { _properties = new ConcurrentDictionary<StringBuilder, int>(); } public int Duration { get; set; } public string VaryByParam { get; set; } public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var actionArguments = context.ActionArguments.Values.Single(); var properties = VaryByParam.Split(",").ToList(); StringBuilder key = new StringBuilder(); foreach (var actionArgument in actionArguments.GetType().GetProperties()) { if (!properties.Any(t => t.Trim().ToLower() == actionArgument.Name.ToLower())) continue; var value = actionArguments.GetType().GetProperty(actionArgument.Name).GetValue(actionArguments, null).ToString(); key.Append(value); } _properties.AddOrUpdate(key, 1, (x, y) => y + 1); // rest of code } }
«رمزنگاری فایلهای PDF با استفاده از کلید عمومی توسط iTextSharp»
در مطلب فوق در مورد رمزنگاری اطلاعات فایلهای PDF به کمک iTextSharp بحث شد. در مطلب جاری به نحوه رفع این محدودیتها خواهیم پرداخت.
الف) رمزگشایی با استفاده از کلمه عبور
using System.IO; using iTextSharp.text.pdf; namespace PdfDecryptor.Core { public class PasswordDecryptor { public string ReadPassword { set; get; } public string PdfPath { set; get; } public string OutputPdf { set; get; } public void DecryptPdf() { PdfReader.unethicalreading = true; PdfReader reader; if (string.IsNullOrWhiteSpace(ReadPassword)) reader = new PdfReader(PdfPath); else reader = new PdfReader(PdfPath, System.Text.Encoding.UTF8.GetBytes(ReadPassword)); using (var stamper = new PdfStamper(reader, new FileStream(OutputPdf, FileMode.Create))) { stamper.Close(); } } } }
- اگر PDF ایی صرفا دارای محدودیت چاپ بوده و این قابلیت ویژه آن غیرفعال شده است، فقط کافی است مسیر فایل PDF موجود (PdfPath) و مسیر فایل جدیدی که قرار است تولید شود (OutputPdf) ذکر گردد. خروجی فایلی خواهد بود که هیچگونه محدودیتی ندارد. این مساله هم صرفا توسط PdfReader.unethicalreading میسر شده است. به عبارتی ذکر و تنظیم edit password در فایلهای PDF فاقد امنیت است. همین اندازه که PdfReader میتواند فایلی را بخواند، امکان تهیه یک کپی بدون محدودیت از آن توسط PdfStamper وجود خواهد داشت.
در مورد ReadPassword در پیشنیاز ذکر شده، توضیحات کافی به همراه تصویر وجود دارد؛ حالت خاصی که کاربران برای مشاهده محتویات فایل نیاز خواهند داشت تا کلمهی عبور مرتبط را وارد نمایند. در اینجا ذکر ReadPassword الزامی است. خروجی نهایی کلاس فوق رفع کامل این محدودیت است.
ب) رمزگشایی توسط کلید عمومی
using System.IO; using iTextSharp.text.pdf; namespace PdfDecryptor.Core { public class Decryptor { public string PfxPath { set; get; } public string PfxPassword { set; get; } public string InputPdf { set; get; } public string OutputPdf { set; get; } public void DecryptPdf() { var certs = new PfxReader().ReadCertificate(PfxPath, PfxPassword); var reader = new PdfReader(InputPdf, certs.X509Certificates[0], certs.PrivateKey); using (var stamper = new PdfStamper(reader, new FileStream(OutputPdf, FileMode.Create))) { stamper.Close(); } } } }
در این حالت مسیر فایل PFX به همراه کلمه عبور آن (PfxPassword) باید مشخص شود. خروجی فایلی است بدون محدودیت خاصی.
پ.ن.
این مثال را به صورت یک فایل اجرایی از اینجا میتوانید دریافت کنید.
از SQL Server 2008 به بعد، نوع داده جدیدی به نام geography به نوعهای قابل تعریف ستونها اضافه شدهاست. در این نوع ستونها میتوان طول و عرض جغرافیایی یک نقطه را ذخیره کرد و سپس به کمک توابع توکاری از آنها کوئری گرفت.
در اینجا نمونهای از نحوهی تعریف و همچنین مقدار دهی این نوع ستونها را مشاهده میکنید:
CREATE TABLE [Geo]( [id] [int] IDENTITY(1,1) NOT NULL, [Location] [geography] NULL ) insert into Geo( Location , long, lat ) values ( geography::STGeomFromText ('POINT(-121.527200 45.712113)', 4326))
در اینجا متدهای توکار دیگری مانند geography::STDistance برای یافتن فاصله مستقیم بین نقاط نیز ارائه شدهاند. خروجی آن بر حسب متر است.
پشتیبانی از Spatial Data در Entity framework
پشتیبانی از نوع مخصوص geography، در EF 5 توسط نوع دادهای DbGeography ارائه شد. این نوع دادهای immutable است. به این معنا که پس از نمونه سازی، دیگر مقدار آن قابل تغییر نیست.
در اینجا برای نمونه مدلی را مشاهده میکنید که از نوع دادهای DbGeography استفاده میکند:
using System.Data.Entity.Spatial; namespace EFGeoTests.Models { public class GeoLocation { public int Id { get; set; } public DbGeography Location { get; set; } public string Name { get; set; } public string Type { get; set; } public override string ToString() { return string.Format("Name:{0}, Location:{1}", Name, Location); } } }
using System; using System.Data.Entity; using EFGeoTests.Models; namespace EFGeoTests.Config { public class MyContext : DbContext { public DbSet<GeoLocation> GeoLocations { get; set; } public MyContext() : base("Connection1") { this.Database.Log = sql => Console.Write(sql); } } }
private static DbGeography createPoint(double longitude, double latitude, int coordinateSystemId = 4326) { var text = string.Format(CultureInfo.InvariantCulture.NumberFormat,"POINT({0} {1})", longitude, latitude); return DbGeography.PointFromText(text, coordinateSystemId); }
تهیه منبع دادهی جغرافیایی
برای تدارک یک مثال واقعی جغرافیایی، نیاز به اطلاعاتی دقیق داریم. این نوع اطلاعات عموما توسط یک سری فایل مخصوص به نام Shapefiles که حاوی اطلاعات برداری جغرافیایی هستند ارائه میشوند. برای نمونه اطلاعات جغرافیایی به روز ایران را از آدرس ذیل میتوانید دریافت کنید:
http://download.geofabrik.de/asia/iran.html
http://download.geofabrik.de/asia/iran-latest.shp.zip
پس از دریافت این فایل، به تعدادی فایل با پسوندهای shp، shx و dbf خواهیم رسید.
فایلهای shp بیانگر فرمت اشکال ذخیره شده هستند. فایلهای shx یک سری ایندکس بوده و فایلهای dbf از نوع بانک اطلاعاتی dBase IV میباشند.
همچنین اگر فایلهای prj را باز کنید، یک چنین اطلاعاتی در آن موجودند:
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
خواندن فایلهای shp در دات نت
پس از دریافت فایلهای shp و بانکهای اطلاعاتی مرتبط با اطلاعات جغرافیایی ایران، اکنون نوبت به پردازش این فایلهای مخصوص با فرمت بانک اطلاعاتی فاکس پرو مانند، رسیدهاست. برای این منظور میتوان از پروژهی سورس باز ذیل استفاده کرد:
این پروژه در خواندن فایلهای shp بدون نقص عمل میکند اما توانایی خواندن نامهای فارسی وارد شده در این نوع بانکهای اطلاعاتی را ندارد. برای رفع این مشکل، سورس آن را از Codeplex دریافت کنید. سپس فایل Shapefile.cs را گشوده و ابتدای خاصیت Current آنرا به نحو ذیل تغییر دهید:
/// <summary> /// Gets the current shape in the collection /// </summary> public Shape Current { get { if (_disposed) throw new ObjectDisposedException("Shapefile"); if (!_opened) throw new InvalidOperationException("Shapefile not open."); // get the metadata StringDictionary metadata = null; if (!RawMetadataOnly) { metadata = new StringDictionary(); for (int i = 0; i < _dbReader.FieldCount; i++) { string value = _dbReader.GetValue(i).ToString(); if (_dbReader.GetDataTypeName(i) == "DBTYPE_WVARCHAR") { // برای نمایش متون فارسی نیاز است value = Encoding.UTF8.GetString(Encoding.GetEncoding(720).GetBytes(value)); } metadata.Add(_dbReader.GetName(i), value); } }
using System.Collections.Generic; using System.Linq; using Catfood.Shapefile; namespace EFGeoTests { public class MapPoint { public Dictionary<string, string> Metadata { set; get; } public double X { set; get; } public double Y { set; get; } } public static class ShapeReader { public static IList<MapPoint> ReadShapeFile(string path) { var results = new List<MapPoint>(); using (var shapefile = new Shapefile(path)) { foreach (var shape in shapefile) { if (shape.Type != ShapeType.Point) continue; var shapePoint = shape as ShapePoint; if (shapePoint == null) continue; var metadataNames = shape.GetMetadataNames(); if(!metadataNames.Any()) continue; var metadata = new Dictionary<string, string>(); foreach (var metadataName in metadataNames) { metadata.Add(metadataName,shape.GetMetadata(metadataName)); } results.Add(new MapPoint { Metadata = metadata, X = shapePoint.Point.X, Y = shapePoint.Point.Y }); } } return results; } } }
افزودن اطلاعات جغرافیایی به بانک اطلاعاتی SQL Server به کمک Entity framework
فایل places.shp را در مجموعه فایلهایی که در ابتدای بحث عنوان شدند، میتوانید مشاهده کنید. قصد داریم اطلاعات نقاط آنرا به مدل GeoLocation انتساب داده و سپس ذخیره کنیم:
var points = ShapeReader.ReadShapeFile("IranShapeFiles\\places.shp"); using (var context = new MyContext()) { context.Configuration.AutoDetectChangesEnabled = false; context.Configuration.ProxyCreationEnabled = false; context.Configuration.ValidateOnSaveEnabled = false; if (context.GeoLocations.Any()) return; foreach (var point in points) { context.GeoLocations.Add(new GeoLocation { Name = point.Metadata["name"], Type = point.Metadata["type"], Location = createPoint(point.X, point.Y) }); } context.SaveChanges(); }
در فایلهای مرتبط با places.shp، متادیتا name، معادل نام شهرهای ایران است و type آن بیانگر شهر، روستا و امثال آن میباشد.
پس از اینکه اطلاعات مکانهای ایران، در SQL Server ذخیره شدند، نمایش بصری آنها را در management studio نیز میتوان مشاهده کرد:
کوئری گرفتن از اطلاعات جغرافیایی
فرض کنید میخواهیم مکانهایی را با فاصله کمتر از 5 کیلومتر از تهران پیدا کنیم:
var tehran = createPoint(51.4179604, 35.6884243); using (var context = new MyContext()) { // find any locations within 5 kilometers ordered by distance var locations = context.GeoLocations .Where(loc => loc.Location.Distance(tehran) < 5000) .OrderBy(loc => loc.Location.Distance(tehran)) .ToList(); foreach (var location in locations) { Console.WriteLine(location.Name); } }
و یا اگر بخواهیم دقیقا بر اساس مختصات یک نقطه، مکانی را بیابیم، میتوان از متد SpatialEquals استفاده کرد:
var tehran = createPoint(51.4179604, 35.6884243); using (var context = new MyContext()) { // find any locations within 5 kilometers ordered by distance var tehranLocation = context.GeoLocations.FirstOrDefault(loc => loc.Location.SpatialEquals(tehran)); if (tehranLocation != null) { Console.WriteLine(tehranLocation.Type); } }
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
EFGeoTests.zip
public class Rider { public int Id { get; set; } public EquineBeast Mount { get; set; } } public enum EquineBeast { Donkey, Mule, Horse, Unicorn }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion( v => v.ToString(), v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v)); }
نکته 1: مقادیر نال، هیچگاه به تبدیلگرهای مقدار، ارسال نمیشوند. اینکار پیاده سازی آنها را سادهتر میکند و همچنین میتوان آنها را بین خواص نالپذیر و نالنپذیر، به اشتراک گذاشت. بنابراین برای مقادیر نال نمیتوان تبدیلگر نوشت.
نکته 2: کاری که در متد HasConversion فوق انجام شدهاست، در حقیقت وهله سازی ضمنی یک ValueConverter و استفاده از آن است. میتوان اینکار را به صورت صریح نیز انجام داد:
var converter = new ValueConverter<EquineBeast, string>( v => v.ToString(), v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v)); modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion(converter);
تبدیلگرهای مقدار توکار EF Core
برای بسیاری از اعمال متداول، در فضای نام Microsoft.EntityFrameworkCore.Storage.ValueConversion، تعدادی تبدیلگر مقدار تدارک دیده شدهاند که به این شرح میباشند:
BoolToZeroOneConverter: تبدیلگر bool به صفر و یک
BoolToStringConverter: تبدیلگر bool به Y و یا N
BoolToTwoValuesConverter: تبدیلگر bool به دو مقداری دلخواه
BytesToStringConverter: تبدیلگر آرایهای از بایتها به یک رشتهی Base64-encoded
CastingConverter: تبدیلگر یک نوع به نوعی دیگر
CharToStringConverter: تبدیلگر char به string
DateTimeOffsetToBinaryConverter: تبدیلگر DateTimeOffset به یک مقدار 64 بیتی باینری
DateTimeOffsetToBytesConverter: تبدیلگر DateTimeOffset به آرایهای از بایتها
DateTimeOffsetToStringConverter: تبدیلگر DateTimeOffset به رشته
DateTimeToBinaryConverter: تبدیلگر DateTime به یک مقدار 64 بیتی با درج DateTimeKind
DateTimeToStringConverter: تبدیلگر DateTime به یک رشته
DateTimeToTicksConverter: تبدیلگر DateTime به ticks آن
EnumToNumberConverter: تبدیلگر Enum به عدد متناظر با آن
EnumToStringConverter: تبدیلگر Enum به رشته
GuidToBytesConverter: تبدیلگر Guid به آرایهای از بایتها
GuidToStringConverter: تبدیلگر Guid به رشته
NumberToBytesConverter: تبدیلگر اعداد به آرایهای از بایتها
NumberToStringConverter: تبدیلگر اعداد به رشته
StringToBytesConverter: تبدیلگر رشته به آرایهای از بایتهای UTF8 معادل آن
TimeSpanToStringConverter: تبدیلگر TimeSpan به رشته
TimeSpanToTicksConverter: تبدیلگر TimeSpan به ticks آن
برای نمونه در این لیست، EnumToStringConverter نیز وجود دارد. بنابراین نیازی به تعریف دستی آن مانند مثال ابتدای بحث نیست و میتوان به صورت زیر از آن استفاده کرد:
var converter = new EnumToStringConverter<EquineBeast>(); modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion(converter);
تعیین نوع تبدیلگر مقدار، جهت ساده سازی تعاریف
برای حالاتی که تبدیلگر مقدار توکاری تعریف شدهاست، صرفا تعریف نوع تبدیل، کفایت میکند:
modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion<string>();
public class Rider { public int Id { get; set; } [Column(TypeName = "nvarchar(24)")] public EquineBeast Mount { get; set; } }
نوشتن تبدیلگر خودکار مقادیر خواص، به نمونهای رمزنگاری شده
پس از آشنایی با مفهوم «تبدیلگرهای مقدار» در +EF Core 2.1، اکنون میتوانیم یک نمونهی سفارشی از آنرا نیز طراحی کنیم:
namespace DbConfig.Web.DataLayer.Context { public class MyAppContext : DbContext { // … protected override void OnModelCreating(ModelBuilder builder) { var encryptedConverter = new ValueConverter<string, string>( convertToProviderExpression: v => new string(v.Reverse().ToArray()), // encrypt convertFromProviderExpression: v => new string(v.Reverse().ToArray()) // decrypt ); // Custom application mappings builder.Entity<ConfigurationValue>(entity => { entity.Property(e => e.Value).IsRequired().HasConversion(encryptedConverter); }); } } }
میتوان قسمت HasConversion را به صورت زیر خودکار کرد:
ابتدا یک Attribute جدید را به نام Encrypted به برنامه اضافه میکنیم:
using System; namespace Test { [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class EncryptedAttribute : Attribute { } }
namespace DbConfig.Web.DomainClasses { public class ConfigurationValue { public int Id { get; set; } public string Key { get; set; } [Encrypted] public string Value { get; set; } } }
namespace DbConfig.Web.DataLayer.Context { public class MyAppContext : DbContext { protected override void OnModelCreating(ModelBuilder builder) { var encryptedConverter = new ValueConverter<string, string>( convertToProviderExpression: v => new string(v.Reverse().ToArray()), // encrypt convertFromProviderExpression: v => new string(v.Reverse().ToArray()) // decrypt ); foreach (var entityType in builder.Model.GetEntityTypes()) { foreach (var property in entityType.GetProperties()) { var attributes = property.PropertyInfo.GetCustomAttributes(typeof(EncryptedAttribute), false); if (attributes.Any()) { property.SetValueConverter(encryptedConverter); } } } }
از دیدگاه برنامه، ValueConverterهای تعریف شده، هیچگونه تاثیری را بر روی کوئری نوشتن و یا ثبت و ویرایش اطلاعات ندارند و عملکرد آنها کاملا از دیدگاه سایر قسمتهای برنامه مخفی است. برای مثال در برنامه، فرمان به روز رسانی خاصیت Value را با مقدار .A new value to test صادر کردهایم (مقدار دهی متداول)، اما همانطور که ملاحظه میکنید، نمونهی رمزنگاری شدهی آن به صورت خودکار در بانک اطلاعاتی درج شدهاست (پارامتر p0):
Executed DbCommand (22ms) [Parameters=[@p1='1', @p0='.tset ot eulav wen A' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='180'] SET NOCOUNT ON; UPDATE [Configurations] SET [Value] = @p0 WHERE [Id] = @p1; SELECT @@ROWCOUNT;
و یا کوئری زیر
db.Set<ConfigurationValue>().Where(x => x.Value.EndsWith("world!"))
SELECT [x].[Id], [x].[Key], [x].[Value] FROM [Configurations] AS [x] WHERE RIGHT([x].[Value], LEN(N'world!')) = N'!dlrow'
ایجاد یک پروژهی ابتدایی Blazor WASM
پروژهای را که در این مطلب تکمیل خواهیم کرد، از نوع Blazor WASM هاست شدهاست. بنابراین در پوشهی فرضی BlazorAsyncValidation، دستور «dotnet new blazorwasm --hosted» را صادر میکنیم تا ساختار ابتدایی پروژه که به همراه یک کلاینت Blazor WASM و یک سرور ASP.NET Core Web API است، تشکیل شود. از قسمت Web API، برای پیاده سازی اعتبارسنجی سمت سرور استفاده خواهیم کرد.
مدل ثبت نام برنامه
مدل ثبت نام برنامه تنها از یک خاصیت نام تشکیل شده و در پروژهی Shared قرار میگیرد تا هم در کلاینت و هم در سرور قابل استفاده باشد:
using System.ComponentModel.DataAnnotations; namespace BlazorAsyncValidation.Shared; public class UserDto { [Required] public string Name { set; get; } = default!; }
کنترلر API ثبت نام برنامه
کنترلر زیر که در پوشهی BlazorAsyncValidation\Server\Controllers قرار میگیرد، منطق بررسی تکراری نبودن نام دریافتی از برنامهی کلاینت را شبیه به منطق remote validation استاندارد MVC، پیاده سازی میکند که در نهایت یک true و یا false را باز میگرداند.
در اینجا خروجی بازگشت داده کاملا در اختیار شما است و نیازی نیست تا حتما ارتباطی با MVC داشته باشد؛ چون مدیریت سمت کلاینت بررسی آنرا خودمان انجام خواهیم داد و نه یک کتابخانهی از پیش نوشته شده و مشخص.
using BlazorAsyncValidation.Shared; using Microsoft.AspNetCore.Mvc; namespace BlazorAsyncValidation.Server.Controllers; [ApiController, Route("api/[controller]/[action]")] public class RegisterController : ControllerBase { [HttpPost] public IActionResult IsUserNameUnique([FromBody] UserDto userModel) { if (string.Equals(userModel?.Name, "Vahid", StringComparison.OrdinalIgnoreCase)) { return Ok(false); } return Ok(true); } }
غنی سازی فرم استاندارد Blazor جهت انجام Remote validation
اگر بخواهیم از EditForm استاندارد Blazor در حالت متداول آن و بدون هیچ تغییری استفاده کنیم، مانند مثال زیر که InputText متصل به خاصیت Name مربوط به Dto برنامه را نمایش میدهد:
@page "/" <PageTitle>Index</PageTitle> <h2>Register</h2> <EditForm EditContext="@EditContext" OnValidSubmit="DoSubmitAsync"> <DataAnnotationsValidator/> <div class="row mb-2"> <label class="col-form-label col-lg-2">Name:</label> <div class="col-lg-10"> <InputText @bind-Value="Model.Name" class="form-control"/> <ValidationMessage For="@(() => Model.Name)"/> </div> </div> <button class="btn btn-secondary" type="submit">Submit</button> </EditForm>
public partial class Index { private const string UserValidationUrl = "/api/Register/IsUserNameUnique"; private ValidationMessageStore? _messageStore; [Inject] private HttpClient HttpClient { set; get; } = default!; private EditContext? EditContext { set; get; } private UserDto Model { get; } = new();
ValidationMessageStore به همراه متد Add است و اگر به آن نام فیلد مدنظر را به همراه پیامی، اضافه کنیم، این اطلاعات را به صورت خطای اعتبارسنجی توسط کامپوننت ValidationMessage نمایش میدهد.
محل مقدار دهی اولیهی این اشیاء نیز در روال رویدادگردان OnInitialized به صورت زیر است:
protected override void OnInitialized() { EditContext = new EditContext(Model); _messageStore = new ValidationMessageStore(EditContext); EditContext.OnFieldChanged += (sender, eventArgs) => { var fieldIdentifier = eventArgs.FieldIdentifier; _messageStore?.Clear(fieldIdentifier); _ = InvokeAsync(async () => { var errors = await OnValidateFieldAsync(fieldIdentifier.FieldName); if (errors?.Any() != true) { return; } foreach (var error in errors) { _messageStore?.Add(fieldIdentifier, error); } EditContext.NotifyValidationStateChanged(); }); StateHasChanged(); }; EditContext.OnValidationStateChanged += (sender, eventArgs) => StateHasChanged(); EditContext.OnValidationRequested += (sender, eventArgs) => _messageStore?.Clear(); }
برای مثال اگر فیلدی تغییر کند، رویداد OnFieldChanged صادر میشود. در همینجا است که کار فراخوانی متد OnValidateFieldAsync که در ادامه معرفی میشود را انجام میدهیم تا کار اعتبارسنجی Async سمت سرور را انجام دهد. اگر نتیجهای به همراه آن بود، توسط messageStore به صورت یک خطای اعتبارسنجی نمایش داده خواهد شد و همچنین EditContext نیز با فراخوانی متد NotifyValidationStateChanged، وادار به بهروز رسانی وضعیت اعتبارسنجی خود میگردد.
متد سفارشی OnValidateFieldAsync که کار اعتبارسنجی سمت سرور را انجام میدهد، به صورت زیر تعریف شدهاست:
private async Task<IList<string>?> OnValidateFieldAsync(string fieldName) { switch (fieldName) { case nameof(UserDto.Name): var response = await HttpClient.PostAsJsonAsync( UserValidationUrl, new UserDto { Name = Model.Name }); var responseContent = await response.Content.ReadAsStringAsync(); if (string.Equals(responseContent, "false", StringComparison.OrdinalIgnoreCase)) { return new List<string> { $"`{Model.Name}` is in use. Please choose another name." }; } // TIP: It's better to use the `DntDebounceInputText` component for this case to reduce the network round-trips. break; } return null; }
یک نکته: InputText استاندارد در حالت معمول آن، پس از تغییر focus به یک کنترل دیگر، سبب بروز رویداد OnFieldChanged میشود و نه در حالت فشرده شدن کلیدها. به همین جهت اگر برنامه پیوستی را میخواهید آزمایش کنید، نیاز است فقط focus را تغییر دهید و یا یک کنترل سفارشی را برای اینکار توسعه دهید.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorAsyncValidation.zip