مطالب
سری بررسی SQL Smell در EF Core - ایجاد روابط Polymorphic - بخش اول
سناریویی را در نظر بگیرید که برای هر کدام از مدلهای Article, Video, Event می‌خواهیم قابلیت کامنت‌گذاری جداگانه‌ای را داشته باشیم. چندین روش برای پیاده‌سازی این سناریو وجود دارد که در ادامه به آنها خواهیم پرداخت. 

Polymorphic association  
در این روش بجای تعریف چند کلید خارجی، تنها یک فیلد جنریک را تعریف خواهیم کرد که می‌تواند همزمان یک ارجاع را به مدل‌های مطرح شده داشته باشد. برای تعیین نوع کلید هم نیاز به یک فیلد دیگر جهت تعیین نوع ارجاع خواهیم داشت. در واقع با کمک آن می‌توانیم تشخیص دهیم که ارجاع موردنظر به کدام موجودیت اشاره دارد: 


public enum CommentType
{
    Article,
    Video,
    Event
}

public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    public int? TypeId { get; set; }
    public CommentType CommentType { get; set; }
}

public class Article
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
}

public class Video
{
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
}

public class Event
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTimeOffset? Start { get; set; }
    public DateTimeOffset? End { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Article> Articles { get; set; }
    public DbSet<Video> Videos { get; set; }
    public DbSet<Event> Events { get; set; }
    public DbSet<Comment> Comments { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=polymorphic.db");
}

این روش در واقع به عنوان یک Anti Pattern و SQL Smell شناخته می‌شود؛ زیرا امکان کوئری گرفتن از دیتابیس را دشوار خواهد کرد. اکثر فریم‌ورک‌های غیر دات‌نتی به صورت توکار قابلیت پیاده‌سازی این نوع ارتباط را ارائه می‌دهند. اما در Entity Framework باید به صورت دستی تنظیمات انجام شوند و همچنین به دلیل نداشتن ارجاع مستقیم (کلید خارجی) درون جدول Comments با مشکل data integrity مواجه خواهیم شد. یکی دیگر از مشکلات آن امکان درج orphaned record است؛ زیرا هیچ Constraintی بر روی Polymorphic Key تعریف نشده‌است. در این روش مدیریت واکشی اطلاعات سخت خواهد بود و در حین کوئری گرفتن دیتا باید CommentType را نیز به همراه TypeId به صورت صریحی قید کنیم:
var articleComments = dbContext.Comments
                .Where(x => x.CommentType == CommentType.Article && x.TypeId.Value == 1);
foreach (var articleComment in articleComments)
{
    Console.WriteLine(articleComment.CommentText);
}

Join Table Per Relationship Type
 یک روش دیگر ایجاد Join Table به ازای هر ارتباط است:


public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    
    public virtual ICollection<ArticleComment> ArticleComments { get; set; }
    public virtual ICollection<VideoComment> VideoComments { get; set; }
    public virtual ICollection<EventComment> EventComments { get; set; }
}

public class Article
{
    public Article()
    {
        ArticleComments = new HashSet<ArticleComment>();
    }
    
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
    
    public virtual ICollection<ArticleComment> ArticleComments { get; set; }

}

public class Video
{
    public Video()
    {
        VideoComments = new HashSet<VideoComment>();
    }
    
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
    
    public virtual ICollection<VideoComment> VideoComments { get; set; }
}

public class Event
{
    public Event()
    {
        EventComments = new HashSet<EventComment>();
    }
    
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTimeOffset? Start { get; set; }
    public DateTimeOffset? End { get; set; }
    
    public virtual ICollection<EventComment> EventComments { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Article> Articles { get; set; }
    public DbSet<ArticleComment> ArticleComments { get; set; }
    public DbSet<Video> Videos { get; set; }
    public DbSet<VideoComment> VideoComments { get; set; }
    public DbSet<Event> Events { get; set; }
    public DbSet<EventComment> EventComments { get; set; }
    public DbSet<Comment> Comments { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=polymorphic.db");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ArticleComment>(entity =>
        {
            entity.HasKey(e => new { e.CommentId, e.ArticleId })
                .HasName("PK_dbo.ArticleComments");

            entity.HasIndex(e => e.ArticleId)
                .HasName("IX_ArticleId");

            entity.HasIndex(e => e.CommentId)
                .HasName("IX_ArticleCommentId");

            entity.HasOne(d => d.Article)
                .WithMany(p => p.ArticleComments)
                .HasForeignKey(d => d.ArticleId)
                .HasConstraintName("FK_dbo.ArticleComments_dbo.Articles_ArticleId");

            entity.HasOne(d => d.Comment)
                .WithMany(p => p.ArticleComments)
                .HasForeignKey(d => d.CommentId)
                .HasConstraintName("FK_dbo.ArticleComments_dbo.Comments_CommentId");
        });
        
        modelBuilder.Entity<VideoComment>(entity =>
        {
            entity.HasKey(e => new { e.CommentId, e.VideoId })
                .HasName("PK_dbo.VideoComments");

            entity.HasIndex(e => e.VideoId)
                .HasName("IX_VideoId");

            entity.HasIndex(e => e.CommentId)
                .HasName("IX_VideoCommentId");

            entity.HasOne(d => d.Video)
                .WithMany(p => p.VideoComments)
                .HasForeignKey(d => d.VideoId)
                .HasConstraintName("FK_dbo.VideoComments_dbo.Videos_VideoId");

            entity.HasOne(d => d.Comment)
                .WithMany(p => p.VideoComments)
                .HasForeignKey(d => d.CommentId)
                .HasConstraintName("FK_dbo.VideoComments_dbo.Comments_CommentId");
        });
        
        modelBuilder.Entity<EventComment>(entity =>
        {
            entity.HasKey(e => new { e.CommentId, e.EventId })
                .HasName("PK_dbo.EventComments");

            entity.HasIndex(e => e.EventId)
                .HasName("IX_EventId");

            entity.HasIndex(e => e.CommentId)
                .HasName("IX_EventCommentId");

            entity.HasOne(d => d.Event)
                .WithMany(p => p.EventComments)
                .HasForeignKey(d => d.EventId)
                .HasConstraintName("FK_dbo.EventComments_dbo.Events_EventId");

            entity.HasOne(d => d.Comment)
                .WithMany(p => p.EventComments)
                .HasForeignKey(d => d.CommentId)
                .HasConstraintName("FK_dbo.EventComments_dbo.Comments_CommentId");
        });
    }
}


همانطور که مشاهده میکنید روش فوق نیاز به اضافه کردن مدلهای بیشتری دارد و همچنین تمام روابط چند به چند نیز نیاز است به صورت کامل تنظیم شوند. مزیت این روش داشتن Constraint برای تمامی کلیدهای خارجی است؛ بنابراین می‌توانیم از صحت دیتا مطمئن شویم:
var article = new Article
{
    Title = "Article A",
    Slug = "article_a",
    Description = "No Description"
};
var comment = new Comment
{
    CommentText = "It's great",
    User = "Sirwan"
};
dbContext.ArticleComments.Add(new ArticleComment
{
    Article = article,
    Comment = comment
});

dbContext.SaveChanges();

var articleOne = dbContext.Articles
    .Include(article => article.ArticleComments)
    .ThenInclude(comment => comment.Comment)
    .First(article => article.Id == 1);
var article1Comments = articleOne.ArticleComments.Select(x => x.Comment);
Console.WriteLine(article1Comments.Count());

Exclusive Belongs To  
یک روش دیگر، اضافه کردن ارجاعی به ازای هر کدام از مدلهای عنوان شده، درون موجودیت Comment می‌باشد که به صورت nullable خواهند بود. بنابراین اگر به عنوان مثال بخواهیم برای یک Article یک کامنت داشته باشیم، کلید رکورد ذخیره شده را به عنوان کلید خارجی در جدول Comments اضافه خواهیم کرد:


public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    
    // Article
    public virtual Article Article { get; set; }
    public int? ArticleId { get; set; }
    
    // Video
    public virtual Video Video { get; set; }
    public int? VideoId { get; set; }
    
    // Event
    public virtual Event Event { get; set; }
    public int? EventId { get; set; }
}
public class Article
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
    public virtual ICollection<Comment> Comments { get; set; }
}

public class Video
{
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
    public virtual ICollection<Comment> Comments { get; set; }
}

public class Event
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTimeOffset? Start { get; set; }
    public DateTimeOffset? End { get; set; }
    public virtual ICollection<Comment> Comments { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Article> Articles { get; set; }
    public DbSet<Video> Videos { get; set; }
    public DbSet<Event> Events { get; set; }
    public DbSet<Comment> Comments { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=polymorphic.db");
}


این روش از لحاظ منطقی و طراحی دیتابیس بدون اشکال است؛ زیرا مقدار نامعتبری را نمی‌توانیم برای کلیدهای خارجی درج کنیم. چون برای کلیدهای تعریف شده درون جدول Comment یکسری Constraint تعریف شده‌اند که صحت دیتای ورودی را بررسی خواهند کرد. حتی در صورت نیاز نیز می‌توانیم یک Constraint ترکیبی را جهت مطمئن شدن از خالی نبودن همزمان ستون‌های FK اضافه کنیم. البته SQLite Provider از HasCheckConstraint پشتیبانی نمی‌کند، ولی اگر به عنوان مثال از MySQL استفاده می‌کنید می‌توانید Constraint موردنظر را اینگونه اضافه کنید: 
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Comment>(entity =>
        entity.HasCheckConstraint("CHECK_FKs", 
            "(`ArticleId`  IS NOT NULL) AND (`VideoId`  IS NOT NULL) AND (`EventId`  IS NOT NULL)"));
}

با طراحی فوق می‌توانیم مطمئن شویم که orphaned record نخواهیم داشت. اما اگر تعداد مدل‌ها بیشتر شوند، باید به ازای هر مدل جدید، یک ارجاع به آن را به جدول Comment اضافه کنیم که در نهایت با تعداد زیادی کلیدهای خارجی مواجه خواهیم شد که در آن واحد فقط یکی از آنها مقدار دارند و بقیه NULL خواهند شد. در مقابل، مزیت این روش، امکان کوئری نویسی ساده‌ی آن است:
var articles = dbContext.Articles
                .Include(x => x.Comments).Where(x => x.Id == 1);
foreach (var article in articles)
{
    Console.WriteLine($"{article.Title} - Comments: {article.Comments.Count}");
}
var comment = dbContext.Comments.Include(x => x.Article)
    .FirstOrDefault(x => x.Id == 1);
Console.WriteLine(comment?.Article.Title);

کدهای مطلب جاری را می‌توانید از اینجا دریافت کنید (هر مثال بر روی برنچی جدا قرار دارد)
نظرات مطالب
امکان استفاده از کتابخانه‌های native در Blazor WASM 6x
مقاله فوق‌العاده ای بود، ممنون از شما
نقش Emscripten این وسط خیلی پر رنگ هست، چون نه تنها کد C و... رو کامپایل به Web Assembly می‌کنه، بلکه در ادامه هم مثلا اگه قراره فایل db مربوط به sqlite جایی ذخیره بشه، برای مثال تعیین می‌کنه که تو قسمت blob storage موجود در indexed db قرار بگیره، چون فرض مثال مرورگر که نمی‌تونه فایل db رو ذخیره کنه تو درایو C برای مثال! در واقع کد مربوطه رو سازگار با دنیای Browser کامپایل می‌کنه 
نظرات مطالب
EF Code First #2
- می‌تونید SetInitializer رو به null مقدار دهی کنید تا کاری به بانک اطلاعاتی نداشته باشد.
و در حالت کلی، بله. مبحثی هست که اخیرا اضافه شده به نام DB Migration که در چند قسمت بعد به آن خواهیم رسید.
اشتراک‌ها
معرفی اعتبارسنجی بدون پسورد استاندارد WebAuthn

For decades, passwords have been the common backbone (headache) of authentication and are well known to lack in security while being frustrating and difficult to use. As we continue to see daily data breaches, the reality of moving away from weak static credentials and killing the password is upon us. Join this session to learn how FIDO2 and WebAuthn open authentication standards, in conjunction with YubiKeys, are solving the elimination of passwords at scale. Hear how organizations like Microsoft have implemented these standards for a true passwordless experience and find out how your organization can follow suit. You'll gain a greater understanding of how to achieve a modern and flexible security architecture through the use of FIDO open standards and hardware authenticators. 

معرفی اعتبارسنجی بدون پسورد استاندارد WebAuthn
اشتراک‌ها
بوت استرپ 4 الفا 2 منتشر شد

Here’s a look at a handful of the changes since our last alpha:

  • Overhauled spacing utilities to use a numerical tiering (to avoid confusion with grid tiers).
  • Continued refactoring efforts to replace markup-specific selectors with classes across several components (including pagination, lists, and more). Still more to do here with additional components.
  • Reverted media queries and grid containers from rems to pixels as viewports are not affected by font-size. See #17403 for details. We’ve got a ton of grid work left, too. Feel free to follow along with#18471.
  • Reverted .0625rem width borders to 1px for more consistent component borders that avoid zoom and font-size bugs across browsers.
  • Renamed .img-responsive to .img-fluid to avoid future confusion on the various responsive image solutions out there.
  • Replaced ZeroClipboard with clipboard.js for Flash-independent copy buttons.
  • Inputs and buttons now share the same border variable to ensure components are always sized similarly.
  • Updated all pseudo-element selectors to use the spec’s preferred double colon (e.g., ::before as opposed to :before).
  • Cards now have outline variants and mixins to support extending base classes further.
  • Utility classes for floats and text alignment now have responsive ranges. This means we’ve dropped the non-responsive classes to avoid duplication.
  • Added support for jQuery 2.
  • And hundreds more Sass improvements, bug fixes, documentation updates, and more. 
بوت استرپ 4 الفا 2 منتشر شد
مطالب
تشخیص اصالت ردیف‌های یک بانک اطلاعاتی در EF Core
همیشه فرض بر این است که مدیر سیستم، فردی است امین و درستکار. این شخص/اشخاص کارهای شبکه، پشتیبان‌گیری، نگهداری و امثال آن‌را انجام داده و از سیستم‌ها محافظت می‌کنند. اکنون این سناریوهای واقعی را درنظر بگیرید:
- پس از خداحافظی با شرکتی که در آن کار می‌کردی، شخصی با پوزخند به شما می‌گوید که «می‌دونستی در برنامه‌ی حق و دستمزد شما، بچه‌های ادمین شبکه، دیتابیس برنامه رو مستقیما دستکاری می‌کردند و تعداد ساعات کاری بیشتری رو وارد می‌کردند»؟!
- مسئول فروشی/مسئول پذیرشی که یاد گرفته چطور به صورت مستقیم به بانک اطلاعاتی دسترسی پیدا کند و آمار فروش/پذیرش روز خودش را در بانک اطلاعاتی، با دستکاری مستقیم و خارج از برنامه، کمتر از مقدار واقعی نمایش دهد.
- باز هم مدیر سیستمی/شبکه‌ای که دسترسی مستقیم به بانک اطلاعاتی دارد، در ساعاتی مشخص، کلمه‌ی عبور هش شده‌ی خودش را مستقیما، بجای کلمه‌ی عبور ادمین برنامه در بانک اطلاعاتی وارد کرده و پس از آن ...

این موارد متاسفانه واقعی هستند! اکنون سؤال اینجا است که آیا برنامه‌ی شما قادر است تشخیص دهد رکوردهایی که هم اکنون در بانک اطلاعاتی ثبت شده‌اند، واقعا توسط برنامه و تمام سطوح دسترسی که برای آن طراحی کرده‌اید، به این شکل درآمده‌اند، یا اینکه توسط اشخاصی به صورت مستقیم و با دور زدن کامل برنامه، از طریق management studioهای مختلف، در سیستم وارد و دستکاری شده‌اند؟! در ادامه راه حلی را برای بررسی این مشکل مهم، مرور خواهیم کرد.


چگونه تغییرات رکوردها را در بانک‌های اطلاعاتی ردیابی کنیم؟

روش متداولی که برای بررسی تغییرات رکوردها مورد استفاده قرار می‌گیرد، هش کردن تمام اطلاعات یک ردیف از جدول است و سپس مقایسه‌ی این هش‌ها با هم. علت استفاده‌ی از الگوریتم‌های هش نیز، حداقل به دو علت است:
- با تغییر حتی یک بیت از اطلاعات، مقدار هش تولید شده تغییر می‌کند.
- طول نهایی مقدار هش شده‌ی اطلاعاتی حجیم، بسیار کم است و به راحتی توسط بانک‌های اطلاعاتی، قابل مدیریت و جستجو است.

اگر از SQL Server استفاده می‌کنید، یک چنین قابلیتی را به صورت توکار به همراه دارد:
SELECT
    [Id], 
    (SELECT top 1  * FROM  [AppUsers] FOR XML auto),
    HASHBYTES ('SHA2_256', (SELECT top 1  * FROM  [AppUsers] FOR XML auto)) AS [hash] -- varbinary(n), since 2012
FROM
    [AppUsers]
با این خروجی


کاری که این کوئری انجام می‌دهد شامل دو مرحله است:
الف) کوئری "SELECT top 1 * FROM [AppUsers] FOR XML auto" کاری شبیه به serialization را انجام می‌دهد. همانطور که مشاهده می‌کنید، نام و مقادیر تمام فیلدهای یک ردیف را به صورت یک خروجی XML در می‌آورد. بنابراین دیگر نیازی نیست تا کار تبدیل مقادیر تمام ستون‌های یک ردیف را به عبارتی قابل هش، به صورت دستی انجام دهیم؛ رشته‌ی XML ای آن هم اکنون آماده‌است.
ب) متد HASHBYTES، این خروجی serialized را با الگوریتم SHA2_256، هش می‌کند. الگوریتم‌های SHA2_256 و همچنین SHA2_512، از سال 2012 به بعد به SQL Server اضافه شده‌اند.

اکنون اگر این هش را به نحوی ذخیره کنیم (برنامه باید این هش را ذخیره و یا به روز رسانی کند) و سپس شخصی به صورت مستقیم ردیف فوق را در بانک اطلاعاتی تغییر دهد، هش جدید این ردیف، با هش قبلی ذخیره شده‌ی توسط برنامه، یکی نخواهد بود که بیانگر دستکاری مستقیم این ردیف، خارج از برنامه و با دور زدن کامل تمام سطوح دسترسی آن است.


چگونه تغییرات رکوردها را در بانک‌های اطلاعاتی، توسط EF Core ردیابی کنیم؟

مزیت روش فوق، توکار بودن آن است که کارآیی فوق العاده‌ای را نیز به همراه دارد. اما چون در ادامه قصد داریم از یک ORM استفاده کنیم و ORMها نیز قرار است توانایی کار کردن با انواع و اقسام بانک‌های اطلاعاتی را داشته باشند، دو مرحله‌ی serialization و هش کردن را در کدهای برنامه و با مدیریت EF Core، مستقل از بانک اطلاعاتی خاصی، انجام خواهیم داد.


معرفی موجودیت‌های برنامه

در مثالی که بررسی خواهیم کرد، دو موجودیت Blog و Post تعریف شده‌اند:
using System.Collections.Generic;

namespace EFCoreRowIntegrity
{
    public interface IAuditableEntity
    {
        string Hash { set; get; }
    }

    public static class AuditableShadowProperties
    {
        public static readonly string CreatedDateTime = nameof(CreatedDateTime);
        public static readonly string ModifiedDateTime = nameof(ModifiedDateTime);
    }

    public class Blog : IAuditableEntity
    {
        public int BlogId { get; set; }
        public string Url { get; set; }

        public List<Post> Posts { get; set; }

        public string Hash { get; set; }
    }

    public class Post : IAuditableEntity
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }

        public string Hash { get; set; }
    }
}
- در اینجا اینترفیس IAuditableEntity را نیز مشاهده می‌کنید که دارای یک خاصیت Hash است. تمام موجودیت‌هایی که قرار است دارای فیلد هش باشند، نیاز است این اینترفیس را پیاده سازی کنند؛ مانند دو موجودیت Blog و Post. در ادامه مقدار خاصیت هش را به صورت خودکار توسط سیستم Tracking، محاسبه و به روز رسانی می‌کنیم.
- به علاوه جهت تکمیل بحث، دو خاصیت سایه‌ای نیز تعریف شده‌اند تا بررسی کنیم که آیا هش این‌ها نیز درست محاسبه می‌شود یا خیر.
- علت اینکه خاصیت Hash، سایه‌ای تعریف نشد، سهولت دسترسی و بالا بردن کارآیی آن بود.



معرفی ظرفی برای نگهداری نام خواص و مقادیر متناظر با یک موجودیت

در ادامه دو کلاس AuditEntry و AuditProperty را مشاهده می‌کنید:
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace EFCoreRowIntegrity
{
    public class AuditEntry
    {
        public EntityEntry EntityEntry { set; get; }
        public IList<AuditProperty> AuditProperties { set; get; } = new List<AuditProperty>();

        public AuditEntry() { }

        public AuditEntry(EntityEntry entry)
        {
            EntityEntry = entry;
        }
    }

    public class AuditProperty
    {
        public string Name { set; get; }
        public object Value { set; get; }

        public bool IsTemporary { set; get; }
        public PropertyEntry PropertyEntry { set; get; }

        public AuditProperty() { }

        public AuditProperty(string name, object value, bool isTemporary, PropertyEntry property)
        {
            Name = name;
            Value = value;
            IsTemporary = isTemporary;
            PropertyEntry = property;
        }
    }
}
زمانیکه توسط سیستم Tracking، موجودیت‌های اضافه شده و یا ویرایش شده را استخراج می‌کنیم، AuditEntry همان موجودیت در حال بررسی است که دارای تعدادی خاصیت یا AuditProperty می‌باشد. این‌ها را توسط دو کلاس فوق برای عملیات بعدی، ذخیره و نگهداری می‌کنیم.


معرفی روشی برای هش کردن مقادیر یک شیء

زمانیکه توسط سیستم Tracking، در حال کاربر بر روی موجودیت‌های اضافه شده و یا ویرایش شده هستیم، می‌خواهیم فیلد هش آن‌ها را نیز به صورت خودکار ویرایش و مقدار دهی کنیم. کلاس زیر، منطق ارائه دهنده‌ی این مقدار هش را بیان می‌کند:
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Newtonsoft.Json;

namespace EFCoreRowIntegrity
{
    public static class HashingExtensions
    {
        public static string GenerateObjectHash(this object @object)
        {
            if (@object == null)
            {
                return string.Empty;
            }

            var jsonData = JsonConvert.SerializeObject(@object, Formatting.Indented);
            using (var hashAlgorithm = new SHA256CryptoServiceProvider())
            {
                var byteValue = Encoding.UTF8.GetBytes(jsonData);
                var byteHash = hashAlgorithm.ComputeHash(byteValue);
                return Convert.ToBase64String(byteHash);
            }
        }

        public static string GenerateEntityEntryHash(this EntityEntry entry, string propertyToIgnore)
        {
            var auditEntry = new Dictionary<string, object>();
            foreach (var property in entry.Properties)
            {
                var propertyName = property.Metadata.Name;
                if (propertyName == propertyToIgnore)
                {
                    continue;
                }
                auditEntry[propertyName] = property.CurrentValue;
            }
            return auditEntry.GenerateObjectHash();
        }

        public static string GenerateEntityHash<TEntity>(this DbContext context, TEntity entity, string propertyToIgnore)
        {
            return context.Entry(entity).GenerateEntityEntryHash(propertyToIgnore);
        }
    }
}
- در اینجا توسط متد JsonConvert.SerializeObject کتابخانه‌ی Newtonsoft.Json، شیء موجودیت را تبدیل به یک رشته‌ی JSON کرده و توسط الگوریتم SHA256 هش می‌کنیم. در آخر هم این مقدار را به صورت Base64 ارائه می‌دهیم.
- نکته‌ی مهم: ما نمی‌خواهیم تمام خواص یک موجودیت را هش کنیم. برای مثال اگر موجودیتی دارای چندین رابطه با جداول دیگری بود، ما مقادیر این‌ها را هش نمی‌کنیم (چون رکوردهای متناظر با آن‌ها در جداول خودشان می‌توانند دارای فیلد هش مخصوصی باشند). بنابراین یک Dictionary را از خواص و مقادیر متناظر با آن‌ها تشکیل داده و این Dictionary را تبدیل به JSON می‌کنیم.
- همچنین در این بین، مقدار خود فیلد Hash یک شیء نیز نباید در هش محاسبه شده، حضور داشته باشد. به همین جهت پارامتر propertyToIgnore را مشاهده می‌کنید.


معرفی Context برنامه که کار هش کردن خودکار موجودیت‌ها را انجام می‌دهد

اکنون نوبت استفاده از تنظیمات انجام شده‌ی تا این مرحله‌است:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Logging;

namespace EFCoreRowIntegrity
{
    public class BloggingContext : DbContext
    {
        public BloggingContext()
        { }

        public BloggingContext(DbContextOptions options)
            : base(options)
        { }

        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.EnableSensitiveDataLogging();
                var path = Path.Combine(Directory.GetCurrentDirectory(), "app_data", "EFCore.RowIntegrity.mdf");
                optionsBuilder.UseSqlServer($"Server=(localdb)\\mssqllocaldb;Database=EFCore.RowIntegrity;AttachDbFilename={path};Trusted_Connection=True;");
                optionsBuilder.UseLoggerFactory(new LoggerFactory().AddConsole((message, logLevel) =>
                logLevel == LogLevel.Debug &&
                           message.StartsWith("Microsoft.EntityFrameworkCore.Database.Command")));
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            foreach (var entityType in modelBuilder.Model
                                                   .GetEntityTypes()
                                                   .Where(e => typeof(IAuditableEntity)
                                                   .IsAssignableFrom(e.ClrType)))
            {
                modelBuilder.Entity(entityType.ClrType)
                            .Property<DateTimeOffset?>(AuditableShadowProperties.CreatedDateTime);
                modelBuilder.Entity(entityType.ClrType)
                            .Property<DateTimeOffset?>(AuditableShadowProperties.ModifiedDateTime);
            }
        }

        public override int SaveChanges()
        {
            var auditEntries = OnBeforeSaveChanges();
            var result = base.SaveChanges();
            OnAfterSaveChanges(auditEntries);
            return result;
        }

        private IList<AuditEntry> OnBeforeSaveChanges()
        {
            var auditEntries = new List<AuditEntry>();

            foreach (var entry in ChangeTracker.Entries<IAuditableEntity>())
            {
                if (entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
                {
                    continue;
                }

                var auditEntry = new AuditEntry(entry);
                auditEntries.Add(auditEntry);

                var now = DateTimeOffset.UtcNow;

                foreach (var property in entry.Properties)
                {
                    var propertyName = property.Metadata.Name;
                    if (propertyName == nameof(IAuditableEntity.Hash))
                    {
                        continue;
                    }

                    if (property.IsTemporary)
                    {
                        // It's an auto-generated value and should be retrieved from the DB after calling the base.SaveChanges().
                        auditEntry.AuditProperties.Add(new AuditProperty(propertyName, null, true, property));
                        continue;
                    }

                    switch (entry.State)
                    {
                        case EntityState.Added:
                            entry.Property(AuditableShadowProperties.CreatedDateTime).CurrentValue = now;
                            auditEntry.AuditProperties.Add(new AuditProperty(propertyName, property.CurrentValue, false, property));
                            break;
                        case EntityState.Modified:
                            auditEntry.AuditProperties.Add(new AuditProperty(propertyName, property.CurrentValue, false, property));
                            entry.Property(AuditableShadowProperties.ModifiedDateTime).CurrentValue = now;
                            break;
                    }
                }
            }

            return auditEntries;
        }

        private void OnAfterSaveChanges(IList<AuditEntry> auditEntries)
        {
            foreach (var auditEntry in auditEntries)
            {
                foreach (var auditProperty in auditEntry.AuditProperties.Where(x => x.IsTemporary))
                {
                    // Now we have the auto-generated value from the DB.
                    auditProperty.Value = auditProperty.PropertyEntry.CurrentValue;
                    auditProperty.IsTemporary = false;
                }
                auditEntry.EntityEntry.Property(nameof(IAuditableEntity.Hash)).CurrentValue =
                    auditEntry.AuditProperties.ToDictionary(x => x.Name, x => x.Value).GenerateObjectHash();
            }
            base.SaveChanges();
        }
    }
}
در اینجا اصل کار، در متد بازنویسی شده‌ی SaveChanges انجام می‌شود:
public override int SaveChanges()
{
    var auditEntries = OnBeforeSaveChanges();
    var result = base.SaveChanges();
    OnAfterSaveChanges(auditEntries);
    return result;
}
در متد OnBeforeSaveChanges، تمام موجودیت‌های تغییر کرده‌ی از نوع IAuditableEntity را که دارای فیلد هش هستند، یافته و نام خاصیت و مقدار متناظر با آن‌ها را در ظرف‌های AuditEntry که پیشتر معرفی شدند، ذخیره می‌کنیم. هنوز در این مرحله کار هش کردن را انجام نخواهیم داد. علت را می‌توانید در بررسی خواص موقتی مشاهده کنید:
if (property.IsTemporary)
{
   // It's an auto-generated value and should be retrieved from the DB after calling the base.SaveChanges().
   auditEntry.AuditProperties.Add(new AuditProperty(propertyName, null, true, property));
   continue;
}
خواص موقتی، عموما تولید شده‌ی توسط دیتابیس هستند. برای مثال زمانیکه یک Id عددی خود افزاینده را به عنوان کلید اصلی جدول معرفی می‌کنید، مقدار آن پس از فراخوانی متد base.SaveChanges، از بانک اطلاعاتی دریافت شده و در اختیار برنامه قرار می‌گیرد. به همین جهت است که نیاز داریم لیست این خواص و مقادیر را یکبار پیش از base.SaveChanges ذخیره کنیم و پس از آن، خواص موقتی را که اکنون دارای مقدار هستند، مقدار دهی کرده و سپس هش نهایی شیء را محاسبه کنیم. اگر پیش از base.SaveChanges این هش را محاسبه کنیم، برای مثال حاوی مقدار Id شیء، نخواهد بود.

همین مقدار تنظیم، برای محاسبه و به روز رسانی خودکار فیلد هش، کفایت می‌کند.


روش بررسی اصالت یک موجودیت

در متد زیر، روش محاسبه‌ی هش واقعی یک موجودیت دریافت شده‌ی از بانک اطلاعاتی را توسط متد الحاقی GenerateEntityHash مشاهده می‌کنید. اگر این هش واقعی (بر اساس مقادیر فعلی این ردیف که حتی ممکن است به صورت دستی و خارج از برنامه تغییر کرده باشد)، با مقدار Hash ثبت شده‌ی پیشین در آن ردیف یکی بود، اصالت این ردیف تائید خواهد شد:
private static void CheckRow1IsAuthentic()
{
    using (var context = new BloggingContext())
    {
        var blog1 = context.Blogs.Single(x => x.BlogId == 1);
        var entityHash = context.GenerateEntityHash(blog1, propertyToIgnore: nameof(IAuditableEntity.Hash));
        var dbRowHash = blog1.Hash;
        Console.WriteLine($"entityHash: {entityHash}\ndbRowHash:  {dbRowHash}");
        if (entityHash == dbRowHash)
        {
            Console.WriteLine("This row is authentic!");
        }
        else
        {
            Console.WriteLine("This row is tampered outside of the application!");
        }
    }
}
یک نمونه خروجی آن به صورت زیر است:
entityHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo=
dbRowHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo=
This row is authentic!

اکنون بانک اطلاعاتی را خارج از برنامه، مستقیما دستکاری می‌کنیم و برای مثال Url اولین ردیف را تغییر می‌دهیم:


در ادامه یکبار دیگر برنامه را اجرا خواهیم کرد:
entityHash: tdiZhKMJRnROGLLam1WpldA0fy/CbjJaR2Y2jNU9izk=
dbRowHash: P110cYquWpoaZuTpCWaqBn6HPSGdoQdmaAN05s1zYqo=
This row is tampered outside of the application!
همانطور که مشاهده می‌کنید، هش واقعی جدید، با هش ثبت شده‌ی در ردیف، یکی نیست؛ که بیانگر ویرایش مستقیم این ردیف می‌باشد.
به علاوه باید درنظر داشت، محاسبه‌ی این هش بدون خود برنامه، کار ساده‌ای نیست. به همین جهت به روز رسانی دستی آن تقریبا غیرممکن است؛ خصوصا اگر متد GenerateObjectHash، کمی با پیچ و تاب بیشتری نیز تهیه شود.


چگونه وضعیت اصالت تعدادی ردیف را بررسی کنیم؟

مثال قبل، در مورد روش بررسی اصالت یک تک ردیف بود. کوئری زیر روش محاسبه‌ی فیلد جدید IsAuthentic را در بین لیستی از ردیف‌ها نمایش می‌دهد:
var blogs = (from blog in context.Blogs.ToList() // Note: this `ToList()` is necessary here for having Shadow properties values, otherwise they will considered `null`.
             let computedHash = context.GenerateEntityHash(blog, nameof(IAuditableEntity.Hash))
             select new
             {
               blog.BlogId,
               blog.Url,
               RowHash = blog.Hash,
               ComputedHash = computedHash,
               IsAuthentic = blog.Hash == computedHash
             }).ToList();


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: EFCoreRowIntegrity.zip
اشتراک‌ها
Aurelia-Toolbelt مجموعه کامپوننتی بر اساس Bootstrap 4 برای Aurelia

Aurelia is one of the best frameworks that we have ever seen in terms of software design, hence, we decided to write a bunch of tools for its developers to pave the way for further usage.

aurelia-toolbelt is that, in which we tried to gather the best libraries in Javascript world together in aurelia fashion. Writing custom-elements, value-converters, and so on. We tried not to invent the wheel, so most of the work is a wrapper, or bridge( am not sure whether the way that we coded can be called bridge or not), around other libraries.

  • Its is utterly important for us to provide a link as a reference to the libraries used, or inspired from, so that other developers can visit their product and decide on their own to use which, besides it's one way that we can respect the time and effort of those programmers.

  • All libraries used in aurelia-toolbelt are open-source and free of charge; most of which are  MIT , however we will provide definition whenever it differs. 

GitHub aurelia-toolbelt

NPM aurelia-toolbelt

Twitter aureliatoolbelt

Aurelia-Toolbelt مجموعه کامپوننتی بر اساس Bootstrap 4 برای Aurelia
اشتراک‌ها
SQLite بیست ساله شد

The first check-ins of SQLite source code happened 20 years ago today, on 2000-05-29.

 
SQLite بیست ساله شد
نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه
- Collation بانک اطلاعاتی ایجاد شده، بر اساس collation «کل سرور» تنظیم می‌شود. برای مثال بر روی سیستم من Server collation به Persian_100_CI_AS تنظیم شده‌است. بنابراین بانک اطلاعاتی ایجاد شده‌ی در اینجا هم دقیقا همین Collation را دارد (امتحان کردم).
/* Identify SQL Server Collation Settings*/
USE Master
GO
SELECT SERVERPROPERTY('collation') AS SQLServerCollation
GO
 - Collation، ارتباطی به نحوه‌ی ذخیره شدن اطلاعات یونیکد ندارد. بیشتر هدف آن sort صحیح اطلاعات است و همچنین مشخص سازی نحوه‌ی مقایسه‌ی عبارات و حساس بودن به بزرگی و کوچکی حروف. شما در Collation فارسی، هم ی فارسی و هم ی عربی را می‌توانید بدون مشکل ثبت کنید. اگر این Collation وجود نداشت، شبیه به SQLite، در مرتب سازی حروف فارسی، عملیات نهایی بر اساس کد اسکی آن‌ها انجام می‌شد و در این حالت مثلا «پ» را در انتهای لیست مشاهده می‌کردید.
 - اگر مشکل ثبت اطلاعات یونیکد را داشت، شما در بانک اطلاعاتی فقط ????? را مشاهده می‌کردید و نه هیچ چیز دیگری را و نه اینکه قسمتی درست ثبت شود و قسمتی نادرست.
 - ویژوال استودیو عموما بر مبنای «تنظیمات محلی سیستم عامل شما» فایل‌ها را ذخیره می‌کند. اگر می‌خواهید این مورد را همواره به UTF8 تغییر دهید، از افزونه‌ی ForceUTF8 (with BOM) استفاده کنید.