مطالب
ASP.NET Web API - قسمت دوم
در قسمت اول به دلایل ایجاد ASP.NET Web API پرداخته شد. در این قسمت، یک مثال ساده از Web API را بررسی می‌کنیم.
تلاش‌های بسیاری توسط توسعه گران صورت پذیرفته است تا فرایند ایجاد وب سرویس WCF در بستر HTTP آسان شود. امروزه وب سرویس هایی که از قالب REST استفاده می‌کنند مطرح هستند.
ASP.NET Web API از مفاهیم موجود در ASP.NET MVC مانند Controllerها استفاده می‌کند و بر مبنای آنها ساخته شده است. بدین شکل، توسعه گر می‌تواند با دانش موجود خود به سادگی وب سرویس‌های مورد نظر را ایجاد کند. Web API، پروتوکل SOAP را به کتاب‌های تاریخی! سپرده است تا از آن به عنوان روشی برای تعامل بین سیستم‌ها یاد شود. امروزه به دلیل فراگیری پروتوکل HTTP، بیشتر محیط‌های برنامه نویسی و سیستم ها، از مبانی اولیه‌ی پروتوکل HTTP مانند اَفعال آن پشتیبانی می‌کنند.
حال قصد داریم تا وب سرویسی را که در قسمت اول با WCF ایجاد کردیم، این بار با استفاده از Web API ایجاد کنیم. به تفاوت این دو دقت کنید.

using System.Web.Http;

namespace MvcApplication1.Controllers
{
    public class ValuesController : ApiController        
    {
        // GET api/values/5
        public string Get(int id)                         
        {
            return string.Format("You entered: {0}", id);
        }
    }
}
اولین تفاوتی که مشهود است، تعداد خطوط کمتر مورد نیاز برای ایجاد وب سرویس با استفاده از Web API است، چون نیاز به interface و کلاس پیاده ساز آن وجود ندارد. در Controller، Web APIهایی که در نقش وب سرویس هستند از کلاس ApiController ارث می‌برند. اَعمال مورد نظر در قالب متدها در Controller تعریف می‌شوند. در مثال قبل، متد Get، یکی از اَعمال است.
نحوه‌ی برگشت یک مقدار از متدها در Web API، مانند WCF است. می‌توانید خروجی متد Get را با اجرای پروژه‌ی قبل در Visual Studio و تست آن با یک مرورگر ملاحظه کنید. دقت داشته باشید که یکی از اصولی که Web API به آن معتقد است این است که وب سرویس‌ها می‌توانند ساده باشند. در Web API، تست و دیباگ وب سرویس‌ها بسیار راحت است. با مرورگر Internet Explorer به آدرس http://localhost:{port}/api/values/3 بروید. پیش از آن، برنامه‌ی Fiddler را اجرا کنید. شکل ذیل، نتیجه را نشان می‌دهد.

در اینجا نتیجه، عبارت "You entered: 3" است که به صورت یک متن ساده برگشت داده شده است.

ایجاد یک پروژه‌ی Web API
در Visual Studio، مسیر ذیل را طی کنید.

File> New> Project> Installed Templates> Visual C#> Web> ASP.NET MVC 4 Web Application 

  نام پروژه را HelloWebAPI بگذارید و بر روی دکمه‌ی OK کلیک کنید (شکل ذیل)

در فرمی که باز می‌شود، گزینه‌ی Web API را انتخاب و بر روی دکمه‌ی OK کلیک کنید (شکل ذیل). البته دقت داشته باشید که ما همیشه مجبور به استفاده از قالب Web API برای ایجاد پروژه‌های خود نیستیم. می‌توان در هر نوع پروژه ای از Web API استفاده کرد.

اضافه کردن مدل
مدل، شی ای است که نمایانگر داده‌ها در برنامه است. Web API می‌تواند به طور خودکار، مدل را به فرمت JSON، XML یا فرمت دلخواهی که خود می‌توانید برای آن ایجاد کنید تبدیل و سپس داده‌های تبدیل شده را در بدنه‌ی پاسخ HTTP به Client ارسال کند. تا زمانی که Client بتواند فرمت دریافتی را بخواند، می‌تواند از آن استفاده کند. بیشتر Clientها می‌توانند فرمت JSON یا XML را پردازش کنند. به علاوه، Client می‌تواند نوع فرمت درخواستی از Server را با تنظیم مقدار هدر Accept در درخواست ارسالی تعیین کند. اجازه بدهید کار خود را با ایجاد یک مدل ساده که نمایانگر یک محصول است آغاز کنیم.
بر روی پوشه‌ی Models کلیک راست کرده و از منوی Add، گزینه‌ی Class را انتخاب کنید.

نام کلاس را Product گذاشته و کدهای ذیل را در آن بنویسید.

namespace HelloWebAPI.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

مدل ما، چهار Property دارد که در کدهای قبل ملاحظه می‌کنید.

اضافه کردن Controller
در پروژه ای که با استفاده از قالب پیش فرض Web API ایجاد می‌شود، دو Controller نیز به طور خودکار در پروژه‌ی Controller قرار می‌گیرند:

  • HomeController: یک Controller معمول ASP.NET MVC است که ارتباطی با Web API ندارد.
  • ValuesController: یک Controller مختص Web API است که به عنوان یک مثال در پروژه قرار داده می‌شود.


توجه: Controllerها در Web API بسیار شبیه به Controllerها در ASP.NET MVC هستند، با این تفاوت که به جای کلاس Controller، از کلاس ApiController ارث می‌برند و بزرگترین تفاوتی که در نگاه اول در متدهای این نوع کلاس‌ها به چشم می‌خورد این است که به جای برگشت Viewها، داده برگشت می‌دهند.

کلاس ValuesController را حذف و یک Controller به پروژه اضافه کنید. بدین منظور، بر روی پوشه‌ی Controllers، کلیک راست کرده و از منوی Add، گزینه‌ی Controller را انتخاب کنید.

توجه: در ASP.NET MVC 4 می‌توانید بر روی هر پوشه‌ی دلخواه در پروژه کلیک راست کرده و از منوی Add، گزینه‌ی Controller را انتخاب کنید. پیشتر فقط با کلیک راست بر روی پوشه‌ی Controller، این گزینه در دسترس بود. حال می‌توان کلاس‌های مرتبط با Controllerهای معمول را در یک پوشه و Controllerهای مربوط به قابلیت Web API را در پوشه‌ی دیگری قرار داد.

نام Controller را ProductsController بگذارید، از قسمت Template، گزینه‌ی Empty API Controller را انتخاب و بر روی دکمه‌ی OK کلیک کنید (شکل ذیل).

فایلی با نام ProductsController.cs در پوشه‌ی Controllers قرار می‌گیرد. آن را باز کنید و کدهای ذیل را در آن قرار دهید. 

namespace HelloWebAPI.Controllers
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Web.Http;
    using HelloWebAPI.Models;

    public class ProductsController : ApiController
    {

        Product[] products = new Product[] 
        { 
            new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1.39M }, 
            new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, 
            new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } 
        };

        public IEnumerable<Product> GetAllProducts()
        {
            return products;
        }

        public Product GetProductById(int id)
        {
            var product = products.FirstOrDefault((p) => p.Id == id);
            if (product == null)
            {
                var resp = new HttpResponseMessage(HttpStatusCode.NotFound);
                throw new HttpResponseException(resp);
            }
            return product;
        }

        public IEnumerable<Product> GetProductsByCategory(string category)
        {
            return products.Where(
                (p) => string.Equals(p.Category, category, 
                    StringComparison.OrdinalIgnoreCase));
        }
    }
}

برای ساده نگهداشتن مثال، لیستی از محصولات را در یک آرایه قرار داده ایم اما واضح است که در یک پروژه‌ی واقعی، این لیست از پایگاه داده بازیابی می‌شود. در مورد کلاس‌های HttpResponseMessage و HttpResponseException بعداً توضیح می‌دهیم.
در کدهای Controller قبل، سه متد تعریف شده اند: 

  • متد GetAllProducts که کل محصولات را در قالب نوع <IEnumerable<Product برگشت می‌دهد.
  • متد GetProductById که یک محصول را با استفاده از مشخصه‌ی آن (خصیصه‌ی Id) برگشت می‌دهد.
  • متد GetProductsByCategory که تمامی محصولات موجود در یک دسته‌ی خاص را برگشت می‌دهد.

تمام شد! حال شما یک وب سرویس با استفاده از Web API ایجاد کرده اید. هر یک از متدهای قبل در Controller، به یک آدرس به شرح ذیل تناظر دارند.

GetAllProducts به api/products/

GetProductById به api/products/id/

GetProductsByCategory به api/products/?category=category/

در آدرس‌های قبل، id و category، مقادیری هستند که همراه با آدرس وارد می‌شوند و در پارامترهای متناظر خود در متدهای مربوطه قرار می‌گیرند. یک Client می‌تواند هر یک از متدها را با ارسال یک درخواست از نوع GET اجرا کند.

در قسمت بعد، کار خود را با تست پروژه و نحوه‌ی تعامل jQuery با آن ادامه می‌دهیم.

مطالب
ویژگی های کمتر استفاده شده در NET. - بخش اول

ObsoleteAttribute

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

با استفاده از پروپرتی Message آن پیامی را به کاربر استفاده کننده نشان خواهد داد و توصیه می‌شود در این پیام یک راه حل نیز ارائه شود.

پروپرتی IsError در صورتی که مقدار آن به true تعیین شده باشد و کامپایلر در صورتی که عنصری که این خصوصیت بر روی آن تعریف شده است، استفاده شده باشد، در پنجره Error List، پیام مربوط به Obsolete را نشان می‌دهد. برای مثال پس از استفاده از کلاس زیر، OrderDetailTotal به صورت warning و CalculateOrderDetailTotal به صورت Error در پنجره Error List نشان داده می‌شود.

public static class ObsoleteExample
{
    // Mark OrderDetailTotal As Obsolete.
    [ObsoleteAttribute("This property (OrderDetailTotal) is obsolete. Use InvoiceTotal instead.", false)]
    public static decimal OrderDetailTotal
    {
        get  {  return 12m; }
    }

    public static decimal InvoiceTotal
    {
        get  {  return 25m;  }
    }

    // Mark CalculateOrderDetailTotal As Obsolete.
    [ObsoleteAttribute("This method is obsolete. Call CalculateInvoiceTotal instead.", true)]
    public static decimal CalculateOrderDetailTotal()
    {
        return 0m;
    }

    public static decimal CalculateInvoiceTotal()
    {
        return 1m;
    }
}

DefaultValueAttribute

DefaultValueAttribute جهت تعیین مقدار پیش فرض یک پروپرتی استفاده می‌شود. شما می‌توانید یک DefaultValueAttribute را با هر مقداری ایجاد کنید. ایجاد مقدار پیش فرض برای یک پروپرتی باعث نمی‌شود که مقداردهی اولیه‌ای به آن انجام گیرد؛ برای این کار نیاز به کدنویسی می‌باشد.
مثال زیر نحوه استفاده و مقداردهی اولیه پروپرتی‌ها را نشان می‌دهد.
public class DefaultValueAttributeTest
{
    public DefaultValueAttributeTest()
    {
        // Use the DefaultValue propety of each property to actually set it, via reflection.
        foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(this))
        {
            var attr = prop.Attributes[typeof(DefaultValueAttribute)] as DefaultValueAttribute;
            if (attr != null)
                prop.SetValue(this, attr.Value);
        }
    }

    [DefaultValue(28)]
    public int Age { get; set; }

    [DefaultValue("Vahid")]
    public string FirstName { get; set; }

    [DefaultValue("Mohammad Taheri")]
    public string LastName { get; set; }

    public override string ToString()
    {
        return $"{this.FirstName} {this.LastName} is {this.Age}.";
    }
}

DebuggerBrowsableAttribute 

در صورت استفاده از DebuggerBrowsableAttribute ، شما می‌توانید نحوه نمایش یک عضو را در پنجره متغیرها، در زمان دیباگ، تعیین کنید.
public class DebuggerBrowsableTest
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)] // عدم نمایش در زمان دیباگ در پنجره متغیرها
    public string FirstName { get; set; }

    [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] // مقدار پیش فرض
    public string LastName { get; set; }

    [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )] // عدم نمایش در زمان دیباگ در پنجره متغیرها
    public string FullName => FirstName + " " + LastName;

    [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )] // تنها در زمانی که یک آرایه یا لیست باشد نمایش داده می‌شود
    public string[] FullNameArray => new string[] { FirstName + " " + LastName };
}

 اگر از کد مثال بالا استفاده کنید و با استفاده از کلید F11 به صورت خط به خط دستورات را اجرا کنید، مشاهده خواهید کرد متغیر FirstName و FullName در پنجره Autos نشان داده نخواهد شد.

 

Operator ??

عملگر ??  در صورتی که عملوند سمت چپ آن تهی (null) نباشد، مقدار آن را باز می‌گرداند و در غیر اینصورت مقدار عملوند سمت راست خود را باز می‌گرداند. نوع‌های تهی پذیر (nullable) می‌توانند دارای مقدار و یا به صورت تعریف نشده باشند. عملگر ?? وقتی که یک نوع تهی پذیر به یک نوع غیرتهی پذیر انتساب داده می‌شود، مقدار پیش فرض آن را باز می‌گرداند.

int? x = null;
int y = x ?? -1;
Console.WriteLine("y now equals -1 because x was null => {0}", y);
int i = DefaultValueOperatorTest.GetNullableInt() ?? default(int);
Console.WriteLine("i equals now 0 because GetNullableInt() returned null => {0}", i);
string s = DefaultValueOperatorTest.GetStringValue();
Console.WriteLine("Returns 'Unspecified' because s is null => {0}", s ?? "Unspecified");
نظرات مطالب
مقابله با XSS ؛ یکبار برای همیشه!
به احتمال زیاد تابحال در برنامه‌های وب خودتون نیاز به ارسال html به همراه تگ‌های آن نداشتید، ولی در کل:
- غیرفعال کردن validateRequest در سطح یک صفحه
<%@ Page 
Language="C#" 
        AutoEventWireup="true" 
        CodeBehind="editpage.aspx.cs" 
        validateRequest="false" 
        Inherits="MyProject.UI.editpage" %>
- در سطح کل برنامه
<system.web>
    <pages validateRequest="false" enableEventValidation="true" />
    <httpRuntime requestValidationMode="2.0" />
</system.web>
البته asp.net mvc از این لحاظ پیشرفته‌تر است؛ چون اجازه می‌ده در سطح یک خاصیت فقط این بررسی رو خاموش کرد با بکارگیری ویژگی AllowHtml آن.

مطالب
آشنایی با NHibernate - قسمت ششم

آشنایی با Automapping در فریم ورک Fluent NHibernate

اگر قسمت‌های قبل را دنبال کرده باشید، احتمالا به پروسه طولانی ساخت نگاشت‌ها توجه کرده‌اید. با کمک فریم ورک Fluent NHibernate می‌توان پروسه نگاشت domain model خود را به data model متناظر آن به صورت خودکار نیز انجام داد و قسمت عمده‌ای از کار به این صورت حذف خواهد شد. (این مورد یکی از تفاوت‌های مهم NHibernate با نمونه‌های مشابهی است که مایکروسافت تا تاریخ نگارش این مقاله ارائه داده است. برای مثال در نگار‌ش‌های فعلی LINQ to SQL یا Entity framework ، اول دیتابیس مطرح است و بعد ساخت کد از روی آن، در حالیکه در اینجا ابتدا کد و طراحی سیستم مطرح است و بعد نگاشت آن به سیستم داده‌ای و دیتابیس)

امروز قصد داریم یک سیستم ساده ثبت خبر را از صفر با NHibernate پیاده سازی کنیم و همچنین مروری داشته باشیم بر قسمت‌های قبلی.

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

ابتدا یک پروژه کنسول جدید را به نام NHSample2 آغاز کنید. سپس ارجاعاتی را به اسمبلی‌های زیر به آن اضافه نمائید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHibernate.Linq.dll
و ارجاعی به اسمبلی استاندارد System.Data.Services.dll دات نت فریم ورک سه و نیم

سپس پوشه‌ای را به نام Domain به این پروژه اضافه نمائید (کلیک راست روی نام پروژه در VS.Net و سپس مراجعه به منوی Add->New folder). در این پوشه تعاریف موجودیت‌های برنامه را قرار خواهیم داد. سه کلاس جدید Category ، User و News را در این پوشه ایجاد نمائید. محتویات این سه کلاس به شرح زیر هستند:

namespace NHSample2.Domain
{
public class User
{
public virtual int Id { get; set; }
public virtual string UserName { get; set; }
public virtual string Password { get; set; }
}
}


namespace NHSample2.Domain
{
public class Category
{
public virtual int Id { get; set; }
public virtual string CategoryName { get; set; }
}
}


using System;

namespace NHSample2.Domain
{
public class News
{
public virtual Guid Id { get; set; }
public virtual string Subject { get; set; }
public virtual string NewsText { get; set; }
public virtual DateTime DateEntered { get; set; }
public virtual Category Category { get; set; }
public virtual User User { get; set; }
}
}
همانطور که در قسمت‌های قبل نیز ذکر شد، تمام خواص پابلیک کلاس‌های Domain ما به صورت virtual تعریف شده‌اند تا lazy loading را در NHibernate فعال سازیم. در حالت lazy loading ، اطلاعات تنها زمانیکه به آن‌ها نیاز باشد بارگذاری خواهند شد. این مورد در حالتیکه نیاز به نمایش اطلاعات تنها یک شیء وجود داشته باشد بسیار مطلوب می‌باشد، یا هنگام ثبت و به روز رسانی اطلاعات نیز یکی از بهترین روش‌ها است. اما زمانیکه با لیستی از اطلاعات سروکار داشته باشیم باعث کاهش افت کارآیی خواهد شد زیرا برای مثال نمایش آن‌ها سبب خواهد شد که 100 ها کوئری دیگر جهت دریافت اطلاعات هر رکورد در حال نمایش اجرا شود (مفهوم دسترسی به اطلاعات تنها در صورت نیاز به آن‌ها). Lazy loading و eager loading (همانند مثال‌های قبلی) هر دو در NHibernate به سادگی قابل تنظیم هستند (برای مثال LINQ to SQL به صورت پیش فرض همواره lazy load است و تا این تاریخ راه استانداردی برای امکان تغییر و تنظیم این مورد پیش بینی نشده است).

اکنون کلاس جدید Config را به برنامه اضافه نمائید:

using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;

namespace NHSample2
{
class Config
{
public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());

new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly).Configure(cfg);

return cfg;
}

public static void GenerateDbScript(Configuration config, string filePath)
{
bool script = true;//فقط اسکریپت دیتابیس تولید گردد
bool export = false;//نیازی نیست بر روی دیتابیس هم اجرا شود
new SchemaExport(config).SetOutputFile(filePath).Create(script, export);
}

public static void BuildDbSchema(Configuration config)
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool drop = false;//آیا اطلاعات موجود دراپ شوند
new SchemaExport(config).Execute(script, export, drop);
}

public static void CreateSQL2008DbPlusScript(string connectionString, string filePath)
{
Configuration cfg =
GenerateMapping(
MsSqlConfiguration
.MsSql2008
.ConnectionString(connectionString)
.ShowSql()
);
GenerateDbScript(cfg, filePath);
BuildDbSchema(cfg);
}

public static ISessionFactory CreateSessionFactory(IPersistenceConfigurer dbType)
{
return
Fluently.Configure().Database(dbType)
.Mappings(m => m.AutoMappings
.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly))
)
.BuildSessionFactory();
}
}
}

در متد GenerateMapping از قابلیت Automapping موجود در فریم ورک Fluent Nhibernate استفاده شده است (بدون نوشتن حتی یک سطر جهت تعریف این نگاشت‌ها). این متد نوع دیتابیس مورد نظر را جهت ساخت تنظیمات خود دریافت می‌کند. سپس با کمک کلاس AutoPersistenceModel این فریم ورک، به صورت خودکار از اسمبلی برنامه نگاشت‌های لازم را به کلاس‌های موجود در پوشه Domain ما اضافه می‌کند (مرسوم است که این پوشه در یک پروژه Class library مجزا تعریف شود که در این برنامه جهت سهولت کار در خود برنامه قرار گرفته است). قسمت Where ذکر شده به این جهت معرفی گردیده است تا Fluent Nhibernate برای تمامی کلاس‌های موجود در اسمبلی جاری، سعی در تعریف نگاشت‌های لازم نکند. این نگاشت‌ها تنها به کلاس‌های موجود در پوشه دومین ما محدود شده‌اند.
سه متد بعدی آن، جهت ایجاد اسکریپت دیتابیس از روی این نگاشت‌های تعریف شده و سپس اجرای این اسکریپت بر روی دیتابیس جاری معرفی شده، تهیه شده‌اند. برای مثال CreateSQL2008DbPlusScript یک مثال ساده از استفاده دو متد قبلی جهت ایجاد اسکریپت و دیتابیس متناظر اس کیوال سرور 2008 بر اساس نگاشت‌های برنامه است.
با متد CreateSessionFactory در قسمت‌های قبل آشنا شده‌اید. تنها تفاوت آن در این قسمت، استفاده از کلاس AutoPersistenceModel جهت تولید خودکار نگاشت‌ها است.

در ادامه دیتابیس متناظر با موجودیت‌های برنامه را ایجاد خواهیم کرد:

using System;

namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
Config.CreateSQL2008DbPlusScript(
"Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true",
"db.sql");

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

پس از اجرای برنامه، ابتدا فایل اسکریپت دیتابیس به نام db.sql در پوشه اجرایی برنامه تشکیل خواهد شد و سپس این اسکریپت به صورت خودکار بر روی دیتابیس معرفی شده اجرا می‌گردد. دیتابیس دیاگرام حاصل را در شکل زیر می‌توانید ملاحظه نمائید:



همچنین اسکریپت تولید شده آن، صرفنظر از عبارات drop اولیه، به صورت زیر است:

create table [Category] (
Id INT IDENTITY NOT NULL,
CategoryName NVARCHAR(255) null,
primary key (Id)
)

create table [User] (
Id INT IDENTITY NOT NULL,
UserName NVARCHAR(255) null,
Password NVARCHAR(255) null,
primary key (Id)
)

create table [News] (
Id UNIQUEIDENTIFIER not null,
Subject NVARCHAR(255) null,
NewsText NVARCHAR(255) null,
DateEntered DATETIME null,
Category_id INT null,
User_id INT null,
primary key (Id)
)

alter table [News]
add constraint FKE660F9E1C9CF79
foreign key (Category_id)
references [Category]

alter table [News]
add constraint FKE660F95C1A3C92
foreign key (User_id)

references [User]

اکنون یک سری گروه خبری، کاربر و خبر را به دیتابیس خواهیم افزود:

using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample2.Domain;

namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
using (ISessionFactory sessionFactory = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
using (ISession session = sessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
//با توجه به کلیدهای خارجی تعریف شده ابتدا باید گروه‌ها را اضافه کرد
Category ca = new Category() { CategoryName = "Sport" };
session.Save(ca);
Category ca2 = new Category() { CategoryName = "IT" };
session.Save(ca2);
Category ca3 = new Category() { CategoryName = "Business" };
session.Save(ca3);

//سپس یک کاربر را به دیتابیس اضافه می‌کنیم
User u = new User() { Password = "123$5@1", UserName = "VahidNasiri" };
session.Save(u);

//اکنون می‌توان یک خبر جدید را ثبت کرد

News news = new News()
{
Category = ca,
User = u,
DateEntered = DateTime.Now,
Id = Guid.NewGuid(),
NewsText = "متن خبر جدید",
Subject = "عنوانی دلخواه"
};
session.Save(news);

transaction.Commit(); //پایان تراکنش
}
}
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
جهت بررسی انجام عملیات ثبت هم می‌توان به دیتابیس مراجعه کرد، برای مثال:



و یا می‌توان از LINQ استفاده کرد:
برای مثال کاربر VahidNasiri تعریف شده را یافته، اطلاعات آن‌را نمایش دهید؛ سپس نام او را به Vahid ویرایش کرده و دیتابیس را به روز کنید.

برای اینکه کوئری‌های LINQ ما شبیه به LINQ to SQL شوند، کلاس NewsContext را به صورت ذیل تشکیل می‌دهیم. این کلاس از کلاس پایه NHibernateContext مشتق شده و سپس به ازای تمام موجودیت‌های برنامه، یک متد از نوع IOrderedQueryable را تشکیل خواهیم داد.

using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample2.Domain;

namespace NHSample2
{
class NewsContext : NHibernateContext
{
public NewsContext(ISession session)
: base(session)
{ }

public IOrderedQueryable<News> News
{
get { return Session.Linq<News>(); }
}

public IOrderedQueryable<Category> Categories
{
get { return Session.Linq<Category>(); }
}

public IOrderedQueryable<User> Users
{
get { return Session.Linq<User>(); }
}
}
}
اکنون جهت یافتن کاربر و به روز رسانی اطلاعات او در دیتابیس خواهیم داشت:

using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using System.Linq;
using NHSample2.Domain;

namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
using (ISessionFactory sessionFactory = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
using (ISession session = sessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
using (NewsContext db = new NewsContext(session))
{
var query = from x in db.Users
where x.UserName == "VahidNasiri"
select x;

//اگر چیزی یافت شد
if (query.Any())
{
User vahid = query.First();
//نمایش اطلاعات کاربر
Console.WriteLine("Id: {0}, UserName: {0}", vahid.Id, vahid.UserName);
//به روز رسانی نام کاربر
vahid.UserName = "Vahid";
session.Update(vahid);

transaction.Commit(); //پایان تراکنش
}
}
}
}
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
مباحث تکمیلی AutoMapping

اگر به اسکریپت دیتابیس تولید شده دقت کرده باشید، عملیات AutoMapping یک سری پیش فرض‌هایی را اعمال کرده است. برای مثال فیلد Id را از نوع identity و به صورت کلید تعریف کرده، یا رشته‌ها را به صورت nvarchar با طول 255 ایجاد نموده است. امکان سفارشی سازی این موارد نیز وجود دارد.

مثال:

using FluentNHibernate.Conventions.Helpers;

public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());

new AutoPersistenceModel()
.Conventions.Add()
.Where(x => x.Namespace.EndsWith("Domain"))
.Conventions.Add(
PrimaryKey.Name.Is(x => "ID"),
DefaultLazy.Always(),
ForeignKey.EndsWith("ID"),
Table.Is(t => "tbl" + t.EntityType.Name)
)
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly)
.Configure(cfg);

return cfg;
}

تابع GenerateMapping معرفی شده را اینجا با قسمت Conventions.Add تکمیل کرده‌ایم. به این صورت دقیقا مشخص شده است که فیلدهایی با نام ID باید primary key در نظر گرفته شوند، همواره lazy loading صورت گیرد و نام کلید خارجی به ID ختم شود. همچنین نام جداول با tbl شروع گردد.
روش دیگری نیز برای معرفی این قرار دادها و پیش فرض‌ها وجود دارد. فرض کنید می‌خواهیم طول رشته پیش فرض را از 255 به 500 تغییر دهیم. برای اینکار باید اینترفیس IPropertyConvention را پیاده سازی کرد:

using FluentNHibernate.Conventions;
using FluentNHibernate.Conventions.Instances;

namespace NHSample2.Conventions
{
class MyStringLengthConvention : IPropertyConvention
{
public void Apply(IPropertyInstance instance)
{
instance.Length(500);
}
}
}
سپس نحوه‌ی معرفی آن به صورت زیر خواهد بود:

public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());

new AutoPersistenceModel()
.Conventions.Add()
.Where(x => x.Namespace.EndsWith("Domain"))
.Conventions.Add<MyStringLengthConvention>()
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly)
.Configure(cfg);

return cfg;
}

نکته:
اگر برای یافتن اطلاعات بیشتر در این مورد در وب جستجو کنید، اکثر مثال‌هایی را که مشاهده خواهید کرد بر اساس نگارش بتای fluent NHibernate هستند و هیچکدام با نگارش نهایی این فریم ورک کار نمی‌کنند. در نگارش رسمی نهایی ارائه شده، تغییرات بسیاری صورت گرفته که آن‌ها را در این آدرس می‌توان مشاهده کرد.

دریافت سورس برنامه قسمت ششم


ادامه دارد ...

نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت سوم - غنی سازی کامپوننت‌ها
با سلام
من همه موارد ذکر شده رو انجام دادم و وقتی که پروژه رو اجرا میکنم خطاهای زیر رو دریافت می‌کنم:
Template parse warnings:
"#" inside of expressions is deprecated. Use "let" instead! ("
                </thead>
                <tbody>
                    <tr [ERROR ->]*ngfor="#product of products">
                        <td>
                            <img [src]="): ProductListComponent@33:24

lang.js (line 374)

EXCEPTION: Template parse errors:
Can't bind to 'ngforOf' since it isn't a known native property ("
                </thead>
                <tbody>
                    <tr [ERROR ->]*ngfor="#product of products">
                        <td>
                            <img [src]="): ProductListComponent@33:24
Property binding ngforOf not used by any directive on an embedded template ("
                </thead>
                <tbody>
                    [ERROR ->]<tr *ngfor="#product of products">
                        <td>
                            <img [s"): ProductListComponent@33:20
Can't bind to 'ng-if' since it isn't a known native property ("
        </div>
        <div class="table-responsive">
            <table class="table" [ERROR ->]*ng-if="products && products.length">
                <thead>
                    <tr>
"): ProductListComponent@17:33
Property binding ng-if not used by any directive on an embedded template ("
        </div>
        <div class="table-responsive">
            [ERROR ->]<table class="table" *ng-if="products && products.length">
                <thead>
                "): ProductListComponent@17:12
و وقتی هم که گالپ را اجرا می‌کنم خطاهای زیر را می‌دهد:
D:/Projects/TestAngular2/node_modules/@angular/core/src/application_ref.d.ts(39,88): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/application_ref.d.ts(99,42): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/application_ref.d.ts(174,33): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/change_detection/differs/default_keyvalue_differ.d.ts(24,15): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/change_detection/differs/default_keyvalue_differ.d.ts(26,16): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/di/reflective_provider.d.ts(105,123): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/di/reflective_provider.d.ts(105,165): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/async.d.ts(27,33): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/async.d.ts(28,45): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(1,25): error TS2304: Cannot find name 'MapConstructor'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(2,25): error TS2304: Cannot find name 'SetConstructor'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(4,27): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(4,39): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(7,9): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(8,30): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(11,43): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(12,27): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(14,23): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(15,25): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(100,41): error TS2304: Cannot find name 'Set'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(101,22): error TS2304: Cannot find name 'Set'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/collection.d.ts(102,25): error TS2304: Cannot find name 'Set'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/lang.d.ts(4,17): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/lang.d.ts(5,17): error TS2304: Cannot find name 'Set'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/lang.d.ts(70,59): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/promise.d.ts(2,14): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/promise.d.ts(8,32): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/promise.d.ts(9,38): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/promise.d.ts(10,35): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/promise.d.ts(10,93): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/promise.d.ts(11,34): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/promise.d.ts(11,50): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/promise.d.ts(12,32): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/promise.d.ts(12,149): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/facade/promise.d.ts(13,43): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/linker/component_resolver.d.ts(8,53): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/linker/component_resolver.d.ts(12,44): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/linker/dynamic_component_loader.d.ts(62,148): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/linker/dynamic_component_loader.d.ts(103,144): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/linker/dynamic_component_loader.d.ts(108,139): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/linker/dynamic_component_loader.d.ts(109,135): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/reflection/reflector.d.ts(28,22): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/reflection/reflector.d.ts(30,15): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/reflection/reflector.d.ts(32,15): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/reflection/reflector.d.ts(34,15): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/reflection/reflector.d.ts(36,16): error TS2304: Cannot find name 'Set'.
D:/Projects/TestAngular2/node_modules/@angular/core/src/testability/testability.d.ts(40,20): error TS2304: Cannot find name 'Map'.
D:/Projects/TestAngular2/node_modules/@angular/platform-browser-dynamic/platform_browser_dynamic.d.ts(75,90): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/rxjs/Observable.d.ts(10,66): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/rxjs/Observable.d.ts(66,60): error TS2304: Cannot find name 'Promise'.
D:/Projects/TestAngular2/node_modules/rxjs/Observable.d.ts(66,70): error TS2304: Cannot find name 'Promise'.
در حالی که کلیه پکیج‌ها نصب شده است.
با تشکر
مطالب
تشخیص اصالت ردیف‌های یک بانک اطلاعاتی در 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
مطالب
آغاز کار با الکترون
در مقاله «آشنایی با الکترون» با نحوه نصب و راه اندازی آن آشنا شدیم. در این مقاله با تعدادی اصطلاح، آشنا شده و یک برنامه ساده را برای نوشتن و خواندن فایل‌ها، می‌نویسیم.
فرآیندها (Processes) در الکترون به دو بخش تقسیم می‌شوند:

یک. فرآیند اصلی (Main Process) که همان فایل جاوااسکریپتی است و توسط main، در فایل package.json مشخص شده‌است .فرآیند اصلی تنها فرآیندی است که قابلیت دسترسی به امکانات گرافیکی سیستم عامل را از قبیل نوتیفیکشن ها، دیالوگ‌ها ،Tray و ... دارد. فرآیند اصلی می‌تواند با استفاده از شیء BrowserWindow که در قسمت قبلی کاربرد آن را مشاهده کردیم، render process را ایجاد کند. با هر بار ایجاد یک نمونه از این شیء، یک Render Process ایجاد می‌شود.

دو. فرآیند رندر (Render Process): از آنجا که الکترون از کرومیوم استفاده می‌کند و کرومیوم شامل معماری چند پردازشی است، هر صفحه‌ی وب می‌تواند پردازش خود را داشته باشد که به آن Render Process می‌گویند. به طور معمول در مرورگرها، صفحات وب در محیطی به نام SandBox اجرا می‌شوندکه اجازه دسترسی به منابع بومی را ندارند. ولی از آنجا که الکترون می‌تواند از Node.js استفاده کند، قابلیت دسترسی به تعاملات سطح پایین سیستم عامل را نیز داراست.

در فرآیند اصلی، پنجره‌ها توسط BrowserWindow ایجاد می‌شوند و هر پنجره‌ای که صفحه وبی را برای خودش باز می‌کند، شامل Render Process خودش است و هر پنجره‌ای که کارش خاتمه یابد، فرآیند مربوط به خودش به اتمام می‌رسد. فرآیند اصلی، همه صفحات وب به همراه Render Process مربوط به خودشان را مدیریت می‌کند و هر فرآیند رندر، از دیگری مجزا و محافظت شده است و تنها تمرکزش بر روی صفحه وبی است که متعلق به خودش است.


در ابتدا قصد داریم یک منو برای برنامه‌ی خود درست کنیم. برای ساخت منو، راه‌های متفاوتی وجود دارند که فعلا ما راه استفاده از template را بر می‌گزینیم که به صورت یک آرایه نوشته می‌شود. کدهای زیر را در فایل index.js یا هر اسمی که برای آن انتخاب کرده‌اید بنویسید:
const electron = require('electron');
const {app,dialog,BrowserWindow,Menu,shell} = electron;

let win;

app.on('ready', function () {
  win = new BrowserWindow({width: 800, height: 600});
  win.loadURL(`file://${__dirname}/index.html`);

var app_menu=[
  {
    label:'پرونده',
    submenu:[
      {
        label:'باز کردن',
        accelerator:'CmdOrCtrl+O',
        click:()=>{
        }
      },
      {
        label:'ذخیره',
        accelerator:'CmdOrCtrl+S',
        click:()=>{
        }
      }
    ]
  },
  {
    label:'سیستم',
    submenu:[
        {
        label:'درباره ما',
        click:()=>
        {
                   shell.openExternal('https://www.dntips.ir');
        }
      },
      {
        label:'خروج',
        accelerator:'CmdOrCtrl+X',
        click:()=>
        {
          win=null;
          app.quit();
        }
      }
    ]
  }
];
تا به اینجای کار، بیشتر کدها برای شما آشناست و فقط تغییرات اندکی در آن‌ها ایجاد شده‌است. مثلا شیء app و سایر اشیاء به طور خلاصه‌تری نوشته شده‌اند. در اینجا دو شیء menu و dialogو shell برای شما جدید هستند. بعد از آن ما یک آرایه را برای منو تدارک دیده‌ایم که نحوه ساخت آن و تعاریفی مثل عنوان، کلید میانبر یا ترکیبی و نحوه انتساب رویدادها را می‌بینید.
 
در خطوط بعدی، یک کار اضافه‌تر را جهت آشنایی بیشتر انجام می‌دهیم. قصد داریم اگر سیستم عامل مکینتاش بود، نام برنامه هم در ابتدای نوار منو نمایش داده شود. به همین جهت در ادامه خطوط زیر را اضافه می‌کنیم:
  if(process.platform=="darwin")
  {
    const app_name=app.getName();
    app_menu.unshift({
      label:app_name
    })
  }
با استفاده از process.platform در node.js می‌توانیم نوع پلتفرم جاری را دریافت کنیم. مقادیر زیر، مقادیری هستند که بازگردانده می‌شوند:

ویندوز
win32 حتی اگر 64 بیتی باشد.
 لینوکس  linux
 مک  darwin
 فری بی اس دی
 freebsd
سولاریس
 sunos
سپس نام برنامه را از شیء app دریافت می‌کنیم و با استفاده از متد unshift، مقادیر داده شده را به ابتدای آرایه اضافه می‌کنیم.

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

دستور app.quit همانطور که از نامش پیداست، باعث خاتمه برنامه می‌شود. ولی یک نکته در اینجا وجود دارد که الزامی به نوشتن کدی برای اینکار نیست. می‌توانید زیرمنوی بالا را به شکل زیر هم بنویسید:
{
        label:'خروج',
        accelerator:'CmdOrCtrl+X',
        role:'close'
 }
خصوصیت role شامل چندین نوع اکشن مانند minimize,close,undo,redo و... می‌باشد که لیست کاملتر آن در اینجا قرار دارد. اگر خصوصیت کلیک و role را همزمان استفاده کنید، خصوصیت role نادیده گرفته خواهد شد.

در انتها با اجرای دو دستور زیر، منو ساخته می‌شود:
  var menu=Menu.buildFromTemplate(app_menu);
  Menu.setApplicationMenu(menu);
در خط اول، منو توسط قالبی که با آرایه‌ها ایجاد کردیم ساخته می‌شود و در خط دوم، منو به برنامه ست می‌شود.
حال قصد داریم برای زیرمنوی «باز کردن فایل» یک دیالوگ open درخواست کنیم. برای این کار از شیء dialog استفاده می‌کنیم. پس خطوط زیر را به رویداد کلیک این زیرمنو اضافه می‌کنیم:
 dialog.showOpenDialog({
             title:'باز کردن فایل متنی',
              properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
             ,filters:[
             {name:'فایل‌های نوشتاری' , extensions:['txt','text']},
             {name:'جهت تست' , extensions:['doc','docx']}
              ]
           },
             (filename)=>{
               if(filename===undefined)
                  return;
               dialog.showMessageBox({title:'پیام اطلاعاتی',type:"info",buttons:['تایید'],message:`the name of file is [${filename}]`});
            });
این متد سه پارامتر دارد که اولین و آخرین پارامتر آن اختیاری می‌باشد. اولین پارامتر آن شیء پنجره است. دومین پارامتر آن، تنظیم یک سری خصوصیات که شامل (پسوند‌های قابل قبول، عنوان، مسیر پیش فرض، قابلیت انتخاب چندگانه، قابلیت باز کردن دایرکتوری و...) می‌شود که لیست کامل آن را می‌توانید در این صفحه ببینید. سومین پارامتر هم که در کد بالا ذکر شده است، callback می‌باشد که خروجی آن، مسیر فایل مورد نظر است و اگر انتخاب چندگانه باشد، آرایه‌ای با نام فایل‌هاست، که همگی آن‌ها به همراه مسیرشان می‌باشند. در صورتیکه کاربر از دیالوگ انصراف بدهد، پارامتر دریافتی با خروجی undefined همراه است.  آخرین دیالوگ هم نمایش یک پیام ساده است که نام فایل جاری را بر میگرداند. اگر خصوصیت buttons را با آرایه خالی مقداردهی کنید، دکمه Ok نمایش داده می‌شود و اگر هم مقداردهی نکنید با خطا روبرو خواهید شد.
برای قسمت ذخیره هم کد زیر را می‌نویسیم:
    dialog.showSaveDialog({
            title:'باز کردن فایل متنی',
             properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
            ,filters:[
            {name:'فایل‌های نوشتاری' , extensions:['txt','text']}
             ]
          },
            (filename)=>{
              if(filename===undefined)
                 return;

           });

حال بهتر است این دیالوگ‌های جاری را هدفمند کنیم و بتوانیم فایل‌های متنی را به کاربر نمایش دهیم، یا آن‌ها را ذخیره کنیم. به همین علت فایل html زیر را نوشته و طبق دستوری که در مقاله «آشنایی با الکترون» فرا گرفتیم، آن را نمایش می‌دهیم:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    Fie Content:<br/>
    <textarea id="TextFile" cols="100" rows="50"></textarea>
 
  </body>
</html>
برای تشکیل ساختار HTML می‌توانید عبارت HTML را تایپ نمایید تا بعد از زدن Enter، ساختار آن به طور خودکار تشکیل شود. سپس محتوا را مثل بالا به شکل دلخواه تغییر می‌دهیم.

کاری که می‌خواهیم انجام دهیم این است که فایل متنی را باز کرده و محتوای آن را در کادر متنی نشان دهیم و موقع ذخیره نیز محتوای نوشته شده در کادر متنی را در فایلی ذخیره کنیم. از آنجا که main Process به المان‌های DOM یا Render Process دسترسی ندارد، باید از طریقی، ارتباط آن را برقرار کنیم. یکی از راه‌های برقراری این ارتباط، IPC است. IPC در واقع یک فرستنده و یک شنونده است که هر کدام در یک سمت قرار گرفته اند. فرستنده پیام را تحت یک عنوان ارسال می‌کند و شنونده منتظر دریافت پیامی تحت همان عنوان میماند و پیام دریافتی را پاسخ می‌دهد. در این مقاله، ما فقط قسمتی از این نوع ارتباطات را بررسی میکنیم.

در نتیجه محتوای callback کدهای دیالوگ open و save را به شکل زیر تغییر می‌دهیم:
Open
 dialog.showOpenDialog({
                 title:'باز کردن فایل متنی',
                  properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
                 ,filters:[
                 {name:'فایل‌های نوشتاری' , extensions:['txt','text']},
                 {name:'جهت تست' , extensions:['doc','docx']}
                  ]
               },
                 (filename)=>{
                   if(filename===undefined)
                      return;

                      win.webContents.send('openFile',filename);
                  // dialog.showMessageBox({title:'پیام اطلاعاتی',type:"info",buttons:['تایید'],message:`the name of file is [${filename}]`});
                });
Save
  dialog.showSaveDialog({
                title:'باز کردن فایل متنی',
                 properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
                ,filters:[
                {name:'فایل‌های نوشتاری' , extensions:['txt','text']}
                 ]
              },
                (filename)=>{
                  if(filename===undefined)
                     return;
                       win.webContents.send('saveFile',filename);
               });
دستور win.webContents.send یک پیام را به صورت Async به سمت RenderProcess مربوطه ارسال میکند. پارامتر اول، عنوان IPC است و پارامتر دوم، پیام IPC است.
برای ایجاد شنونده هم کد زیر را به فایل index.html اضافه می‌کنیم:
  <script>
    const {ipcRenderer} = require('electron');
    var fs=require('fs');

    ipcRenderer.on('openFile', (event, arg) => {
      var content=  fs.readFileSync(String(arg),'utf8');
      document.getElementById("TextFile").value=content;
    });

    ipcRenderer.on('saveFile', (event, arg) =>{
      var content=document.getElementById("TextFile").value;
      fs.writeFileSync(String(arg),content,'utf8');
      alert('ذخیره شد');
    });
    </script>

در اینجا شونده‌هایی را از نوع ipcRenderer ایجاد می‌کنیم و با استفاده از متد on، به پیام‌هایی تحت عنوان‌های مشخص شده گوش فرا می‌دهیم. پیام‌های ارسالی را که حاوی آدرس فایل می‌باشند، به شیءای که از نوع fs می‌باشد، می‌دهند و آن‌ها را می‌خوانند یا می‌نویسند. خواندن و نوشتن فایل، به صورت همزمان صورت میگیرد. ولی اگر دوست دارید که به صورت غیر همزمان پیامی را بخوانید یا بنویسید، باید عبارت Sync را از نام متدها حذف کنید و یک callback را به عنوان پارامتر دوم قرار دهید و محتوای آن را از طریق نوشتن یک پارامتر در سازنده دریافت کنید.

فایل‌های پروژه
 

مطالب
QueryOver در NHibernate و تفاوت‌های آن با LINQ to NH

در NHibernate چندین و چند روش، جهت تهیه کوئری‌ها وجود دارد که QueryOver یکی از آن‌ها است (+). QueryOver نسبت به LINQ to NH سازگاری بهتری با ساز و کار درونی NHibernate دارد؛ برای مثال امکان یکپارچگی آن با سطح دوم کش. هر چند ظاهر QueryOver با LINQ یکی است، اما در عمل متفاوتند و راه و روش خاص خودش را طلب می‌کند. برای مثال در LINQ to NH می‌تواند نوشت x.Property.Contains اما در QueryOver متدی به نام contains قابل استفاده نیست (هر چند در Intellisense ظاهر می‌شود اما عملا تعریف نشده است و نباید آن‌را با LINQ اشتباه گرفت) و سعی در استفاده از آن‌ها به استثناهای زیر ختم می‌شوند:
Unrecognised method call: System.String:Boolean StartsWith(System.String)
Unrecognised method call: System.String:Boolean Contains(System.String)
برای مثال کلاس زیر را در نظر بگیرید؛ کوئری‌های مطلب جاری بر این اساس تهیه خواهند شد:
using NHibernate.Validator.Constraints;

namespace NH3Test.MappingDefinitions.Domain
{
public class Account
{
public virtual int Id { get; set; }

[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }

[NotNull]
public virtual int Balance { set; get; }
}
}

1) یافتن رکوردهایی که در یک مجموعه‌ی مشخص قرار دارند. برای مثال balance آن‌ها مساوی 10 و 12 است:
var list = new[]  { 12,10};
var resultList = session.QueryOver<Account>()
.WhereRestrictionOn(p => p.Balance)
.IsIn(list)
.List();

SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Balance in (
@p0 /* = 10 */, @p1 /* = 12 */
)

2) پیاده سازی همان متد Contains ذکر شده، در QueryOver:
var accountsContianX = session.QueryOver<Account>()
.WhereRestrictionOn(x => x.Name)
.IsLike("X", NHibernate.Criterion.MatchMode.Anywhere)
.List();

SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Name like @p0 /* = %X% */

در اینجا بر اساس مقادیر مختلف MatchMode می‌توان متدهای StartsWith (MatchMode.Start) ، EndsWith (MatchMode.End) ، Equals (MatchMode.Exact) را نیز تهیه نمود.

انجام مثال دوم راه ساده‌تری نیز دارد. قسمت WhereRestrictionOn و IsLike به صورت یک سری extension متد ویژه در فضای نام NHibernate.Criterion تعریف شده‌اند. ابتدا این فضای نام را به کلاس جاری افزوده و سپس می‌توان نوشت :
using NHibernate.Criterion;
...
var accountsContianX = session.QueryOver<Account>()
.Where(x => x.Name.IsLike("%X%"))
.List();

این فضای نام شامل چهار extension method به نام‌های IsLike ، IsInsensitiveLike ، IsIn و IsBetween است.


چگونه extension method سفارشی خود را تهیه کنیم؟

بهترین کار این است که به سورس NHibernate ، فایل‌های RestrictionsExtensions.cs و ExpressionProcessor.cs که تعاریف متد IsLike در آن‌ها وجود دارد مراجعه کرد. در اینجا می‌توان با نحوه‌ی تعریف و سپس ثبت آن در رجیستری extension methods مرتبط با QueryOver توسط متد عمومی RegisterCustomMethodCall آشنا شد. در ادامه سه کار را می‌توان انجام داد:
-متد مورد نظر را در کدهای خود (نه کدهای اصلی NH) اضافه کرده و سپس با فراخوانی RegisterCustomMethodCall آن‌را قابل استفاده نمائید.
-متد خود را به سورس اصلی NH اضافه کرده و کامپایل کنید.
-متد خود را به سورس اصلی NH اضافه کرده و کامپایل کنید (بهتر است همان روش نامگذاری بکار گرفته شده در فایل‌های ذکر شده رعایت شود). یک تست هم برای آن بنویسید (تست نویسی هم یک سری اصولی دارد (+)). سپس یک patch از آن روی آن ساخته (+) و برای تیم NH ارسال نمائید (تا جایی که دقت کردم از کلیه ارسال‌هایی که آزمون واحد نداشته باشند، صرفنظر می‌شود).

مثال:
می‌خواهیم extension متد جدیدی به نام Year را به QueryOver اضافه کنیم. این متد را هم بر اساس توابع توکار بانک‌های اطلاعاتی، تهیه خواهیم نمود. لیست کامل این نوع متدهای بومی SQL را در فایل Dialect.cs سورس‌های NH می‌توان یافت (البته به صورت پیش فرض از متد extract برای جداسازی قسمت‌های مختلف تاریخ استفاده می‌کند. این متد در فایل‌های Dialect مربوط به بانک‌های اطلاعاتی مختلف، متفاوت است و برحسب بانک اطلاعاتی جاری به صورت خودکار تغییر خواهد کرد).
using System;
using System.Linq.Expressions;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Impl;

namespace NH3Test.ConsoleApplication
{
public static class MyQueryOverExts
{
public static bool YearIs(this DateTime projection, int year)
{
throw new Exception("Not to be used directly - use inside QueryOver expression");
}

public static ICriterion ProcessAnsiYear(MethodCallExpression methodCallExpression)
{
string property = ExpressionProcessor.FindMemberExpression(methodCallExpression.Arguments[0]);
object value = ExpressionProcessor.FindValue(methodCallExpression.Arguments[1]);
return Restrictions.Eq(
Projections.SqlFunction("year", NHibernateUtil.DateTime, Projections.Property(property)),
value);
}
}

public class QueryOverExtsRegistry
{
public static void RegistrMyQueryOverExts()
{
ExpressionProcessor.RegisterCustomMethodCall(
() => MyQueryOverExts.YearIs(DateTime.Now, 0),
MyQueryOverExts.ProcessAnsiYear);
}
}
}

اکنون برای استفاده خواهیم داشت:
QueryOverExtsRegistry.RegistrMyQueryOverExts(); //یکبار در ابتدای اجرای برنامه باید ثبت شود
...
var data = session.QueryOver<Account>()
.Where(x => x.AddDate.YearIs(2010))
.List();

برای مثال اگر بانک اطلاعاتی انتخابی از نوع SQLite باشد، خروجی SQL مرتبط به شکل زیر خواهد بود:
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_,
this_.AddDate as AddDate0_0_
FROM
Accounts this_
WHERE
strftime("%Y", this_.AddDate) = @p0 /* =2010 */


هر چند ما تابع year را در متد ProcessAnsiYear ثبت کرده‌ایم اما بر اساس فایل SQLiteDialect.cs ، تعاریف مرتبط و مخصوص این بانک اطلاعاتی (مانند متد strftime فوق) به صورت خودکار دریافت می‌گردد و کد ما مستقل از نوع بانک اطلاعاتی خواهد بود.


نکته جالب!
LINQ to NH هم قابل بسط است؛ کاری که در ORM های دیگر به این سادگی نیست. چند مثال در این زمینه:
چگونه تابع سفارشی SQL Server خود را به صورت یک extension method تعریف و استفاده کنیم: (+) ، یک نمونه دیگر: (+) و نمونه‌ای دیگر: (+).

مطالب
به روز رسانی فیلدهای XML در SQL Server

از SQL server 2005 به بعد، پشتیبانی کاملی از XML توسط این محصول صورت می‌گیرد. در ادامه مروری خواهیم داشت بر نحوه‌ی به روز رسانی مقادیر فیلدهایی از نوع XML در SQL Server .
در ابتدا جدول موقتی زیر را که شامل یک رکورد از نوع XML است، در نظر بگیرید:

DECLARE @tblTest AS TABLE (xmlField XML)

INSERT INTO @tblTest
(
xmlField
)
VALUES
(
'<Sample>
<Node1>Value1</Node1>
<Node2>Value2</Node2>
<Node3>OldValue</Node3>
</Sample>'
)
می‌خواهیم OldValue را به مقداری دیگر تغییر دهیم.

سعی اول:

DECLARE @newValue VARCHAR(50)
SELECT @newValue = 'NewValue'
UPDATE @tblTest
SET xmlField.modify('replace value of (/Sample/Node3)[1] with ' + @newValue)
این سعی با خطای زیر متوقف می‌شود:

The argument 1 of the XML data type method "modify" must be a string literal.
بنابراین از روش String concatenation برای معرفی مقدار متغیر مورد نظر در اینجا نمی‌شود استفاده کرد.

سعی دوم:

DECLARE @newValue VARCHAR(50)
SELECT @newValue = 'NewValue'

UPDATE @tblTest
SET xmlField.modify(
'replace value of (/Sample/Node3)[1] with sql:variable("@newValue")'
)
روش معرفی صحیح یک متغیر را در اینجا می‌توان مشاهده کرد. اما این سعی نیز با خطای زیر متوقف می‌شود:

XQuery [@tblTest.xmlField.modify()]: The target of 'replace value of' must be a non-metadata attribute or an element with simple typed content, found 'element(NodeThree,xdt:untyped) ?'

سعی سوم:

DECLARE @newValue VARCHAR(50)
SELECT @newValue = 'NewValue'

UPDATE @tblTest
SET xmlField.modify(
'replace value of (/Sample/Node3/text())[1]
with sql:variable("@newValue")'
)

SELECT xmlField.value('(/Sample/Node3)[1]','varchar(50)') FROM @tblTest

و بله. کار می‌کنه!
XML ایی را که در ابتدا استفاده کردیم از نوع un-typed XML محسوب شده و هیچ schema ایی را برای آن در نظر نگرفته‌ایم، به همین جهت باید دقیقا مشخص کنیم که قصد داریم text این node را ویرایش نمائیم.

مشکل بعدی!
در ابتدا مثال زیر را در نظر بگیرید:

DECLARE @tblTest AS TABLE (xmlField XML)

INSERT INTO @tblTest
(
xmlField
)
VALUES
(
'<Sample>
<Node1>Value1</Node1>
<Node2>Value2</Node2>
<Node3></Node3>
</Sample>'
)

DECLARE @newValue VARCHAR(50)
SELECT @newValue = 'NewValue'

UPDATE @tblTest
SET xmlField.modify(
'replace value of (/Sample/Node3/text())[1]
with sql:variable("@newValue")'
)

SELECT xmlField.value('(/Sample/Node3)[1]','varchar(50)') FROM @tblTest

این عبارات T-SQL ، خلاصه بحث ما تا به اینجا هستند اما با یک تفاوت. نود 3 در اینجا خالی است.
اگر اسکریپت را اجرا کنید، هیچ تغییری را مشاهده نخواهید کرد. به عبارت دیگر به روز رسانی صورت نمی‌گیرد. در اینجا چون text این نود خالی است ، فرض SQL Server بر این خواهد بود که وجود ندارد، بنابراین این نود را به روز رسانی نخواهد کرد. به همین منظور باید برای به روز رسانی این نود، عبارت جدید را در جایی که text ندارد insert‌ کرد (و نه replace).

DECLARE @newValue VARCHAR(50)
SELECT @newValue = 'NewValue'

UPDATE @tblTest
SET xmlField.modify(
'replace value of (/Sample/Node3/text())[1]
with sql:variable("@newValue")'
)

UPDATE @tblTest
SET xmlField.modify(
'insert text{sql:variable("@newValue")} into
(/Sample/Node3)[1] [not(text())]'
)

SELECT xmlField.value('(/Sample/Node3)[1]','varchar(50)') FROM @tblTest

مطالب
پیدا کردن آیتم‌های تکراری در یک لیست به کمک LINQ

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

برای حذف آیتم‌های تکراری از یک لیست جنریک می‌توان متد زیر را نوشت:
public static List<T> RemoveDuplicates<T>(List<T> items)
{
return (from s in items select s).Distinct().ToList();
}
برای مثال:
public static void TestRemoveDuplicates()
{
List<string> sampleList =
new List<string>() { "A1", "A2", "A3", "A1", "A2", "A3" };
sampleList = RemoveDuplicates(sampleList);
foreach (var item in sampleList)
Console.WriteLine(item);
}
این متد بر روی لیست‌هایی با نوع‌های اولیه مانند string‌ و int و امثال آن درست کار می‌کند. اما اکنون مثال زیر را در نظر بگیرید:
public class Employee
{
public int ID { get; set; }
public string FName { get; set; }
public int Age { get; set; }
}

public static void TestRemoveDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

lstEmp = RemoveDuplicates<Employee>(lstEmp);

foreach (var item in lstEmp)
Console.WriteLine(item.FName);
}
اگر متد TestRemoveDuplicates را اجرا نمائید، رکورد تکراری این لیست جنریک حذف نخواهد شد؛ زیرا متد distinct بکارگرفته شده نمی‌داند اشیایی از نوع کلاس سفارشی Employee را چگونه باید با هم مقایسه نماید تا بتواند موارد تکراری آن‌ها را حذف کند.
برای رفع این مشکل باید از آرگومان دوم متد distinct جهت معرفی وهله‌ای از کلاسی که اینترفیس IEqualityComparer را پیاده سازی می‌کند، کمک گرفت.
public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer);
که نمونه‌ای از پیاده سازی آن به شرح زیر می‌تواند باشد:

public class EmployeeComparer : IEqualityComparer<Employee>
{
public bool Equals(Employee x, Employee y)
{
//آیا دقیقا یک وهله هستند؟
if (Object.ReferenceEquals(x, y)) return true;

//آیا یکی از وهله‌ها نال است؟
if (Object.ReferenceEquals(x, null) ||
Object.ReferenceEquals(y, null))
return false;

return x.Age == y.Age && x.FName == y.FName && x.ID == y.ID;
}

public int GetHashCode(Employee obj)
{
if (Object.ReferenceEquals(obj, null)) return 0;
int hashTextual = obj.FName == null ? 0 : obj.FName.GetHashCode();
int hashDigital = obj.Age.GetHashCode();
return hashTextual ^ hashDigital;
}
}
اکنون اگر یک overload برای متد RemoveDuplicates با درنظر گرفتن IEqualityComparerتهیه کنیم، به شکل زیر خواهد بود:
public static List<T> RemoveDuplicates<T>(List<T> items, IEqualityComparer<T> comparer)
{
return (from s in items select s).Distinct(comparer).ToList();
}
به این صورت متد آزمایشی ما به شکل زیر (که وهله‌ای از کلاس EmployeeComparer‌ به آن ارسال شده) تغییر خواهد کرد:
public static void TestRemoveDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

lstEmp = RemoveDuplicates(lstEmp, new EmployeeComparer());

foreach (var item in lstEmp)
Console.WriteLine(item.FName);
}
پس از این تغییر، حاصل این متد تنها دو رکورد غیرتکراری می‌باشد.

سؤال: برای یافتن آیتم‌های تکراری یک لیست چه باید کرد؟
احتمالا مقاله "روش‌هایی برای حذف رکوردهای تکراری" را به خاطر دارید. اینجا هم می‌توان کوئری LINQ ایی را نوشت که رکوردها را بر اساس سن، گروه بندی کرده و سپس گروه‌هایی را که بیش از یک رکورد دارند، انتخاب نماید.
public static void FindDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

var query = from c in lstEmp
group c by c.Age into g
where g.Count() > 1
select new { Age = g.Key, Count = g.Count() };

foreach (var item in query)
{
Console.WriteLine("Age {0} has {1} records", item.Age, item.Count);
}
}


Vote on iDevCenter