مطالب
مستندسازی خودکار API ها در برنامه‌های مبتنی بر ASP.NET Core بوسیله‌ی Swagger
پیشتر مطلبی در این زمینه در سایت منتشر شد که به خوبی نحوه‌ی پیاده سازی Swagger را در یک برنامه‌ی ASP.NET Web API نشان می‌دهد. حال در این مقاله‌ی کوتاه میخواهیم نحوه‌ی پیاده سازی آن را در یک برنامه‌ی مبتنی بر ASP.NET Core بررسی کنیم.

دریافت Swagger از نوگت

ابتدا باید این پکیج را از آدرسش در نیوگت بگیریم و در برنامه‌ی خود نصب کنیم:
pm> Install-Package Swashbuckle.AspNetCore

پیکربندی برنامه 

برای کانفیگ Swagger و تولید خودکار و پویای مستندات API‌ها توسط آن باید تنظیمات زیر را در کلاس Startup برنامه انجام دهیم :
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Swagger;

namespace MyProject.Web.Api
{
    public class Startup
    {
        public IServiceProvider ConfigureServices(IServiceCollection services)
        {  
            // Register the Swagger generator, defining one or more Swagger documents
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info { Title = "MyProject API Documentation", Version = "v1" });
            });  
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceScopeFactory serviceScopeFactory)
        {
            // Enable middleware to serve generated Swagger as a JSON endpoint.
            app.UseSwagger();

            // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), specifying the Swagger JSON endpoint.
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
            });           
        }
    }
}

مشاهده خروجی مستند سازی API ها

بعد از اینکه کانفیگ‌های فوق را انجام دادیم کافی است تا برنامه را اجرا کرده و آدرس زیر را در مرورگر وارد کنیم:
http://localhost:port/swagger
 در این صورت خروجی به شکل زیر نمایش داده خواهد شد که حاوی اطلاعات بسیار مفیدی در مورد API‌ها می‌باشد. اطلاعاتی شامل http method ، آدرس API، پارامترهای ورودی، مدل خروجی و ...
در صورت استفاده از SWagger ، ذکر [HttpGet]  برای API‌های GET اجباری می‌شود و در صورتیکه این مورد را برای API ای مشخص نکرده باشیم با خطای Run Time مواجه شده و برنامه اجرا نخواهد شد. 

 

نظرات مطالب
دریافت و نمایش فایل‌های PDF در برنامه‌های Blazor WASM
با توجه به اینکه موضوع بحث در مورد فایل‌های PDF است، چنین مشکلی مطلقا با این فایل‌ها، به علت امکان جایگذاری (embedding) فونت‌ها در فایل‌های PDF وجود ندارد (فایل‌های PDF در تمام مرورگرها و حتی تمام سیستم عامل‌ها، به یک شکل به نظر می‌رسند؛ حتی صفحه بندی و نحوه‌ی نمایش آن‌ها هم تغییری نمی‌کند). به همین جهت بهتر است از ابزاری مانند PDFReport برای تولید گزارشات و PDFها استفاده کنید؛ اطلاعات بیشتر.
مطالب
پشتیبانی از حذف و به‌روز رسانی دسته‌ای رکوردها در EF 7.0
همواره حذف و به روز رسانی تعداد زیادی رکورد توسط EF، بسیار غیربهینه و کند بوده‌است؛ از این جهت که یکی از روش‌های انجام اینکار، کوئری گرفتن از رکوردهای مدنظر جهت حذف، سپس بارگذاری آن‌ها در حافظه و در آخر حذف یکی یکی آن‌ها بوده‌است:
using var dbContext = new MyDbContext();
var objectToDelete = await dbContext.Objects.FirstAsync(o => o.Id == id);
dbContext.Objects.Remove(objectToDelete);
await dbContext.SaveChangesAsync();
در اینجا در ابتدا، شیء‌ای که قرار است حذف شود، از بانک اطلاعاتی کوئری گرفته می‌شود تا وارد سیستم Change Tracking شود. سپس از این سیستم ردیابی اطلاعات درون حافظه‌ای، حذف خواهد شد و در نهایت این تغییرات به بانک اطلاعاتی اعمال می‌شوند. بنابراین در این مثال ساده، حداقل دوبار رفت و برگشت به بانک اطلاعاتی وجود خواهد داشت.
البته راه دومی نیز برای انجام اینکار وجود دارد:
using var dbContext = new MyDbContext();
var objectToDelete = new MyObject { Id = id };
dbContext.Objects.Remove(objectToDelete);
await dbContext.SaveChangesAsync();
در این مثال، رفت و برگشت ابتدایی، حذف شده‌است و با فرض معلوم بودن کلید اصلی رکورد مدنظر، آن‌را وارد سیستم Change Tracking کرده و درنهایت آن‌را حذف می‌کنیم. کار متد Remove در اینجا، علامتگذاری این شیء دارای Id، به صورت EntityState.Deleted است.

اکنون می‌توان در EF 7.0، روش سومی را نیز به این لیست اضافه کرد که فقط یکبار رفت و برگشت به بانک اطلاعاتی را سبب می‌شود:
await dbContext.Objects.Where(x => x.Id == id).ExecuteDeleteAsync();


معرفی متدهای حذف و به‌روز رسانی دسته‌ای رکوردها در EF 7.0

EF 7.0 به همراه دو متد جدید ExecuteUpdate و ExecuteDelete (و همچنین نگارش‌های async آن‌ها) است که کار به‌روز رسانی و یا حذف دسته‌ای رکوردها را بدون دخالت سیستم Change tacking میسر می‌کنند. مزیت مهم این روش، عدم نیاز به کوئری گرفتن از بانک اطلاعاتی جهت بارگذاری رکوردهای مدنظر در حافظه و سپس حذف یکی یکی آن‌ها است. فقط باید دقت داشت که چون این روش خارج از سیستم Change tracking صورت می‌گیرد، نتیجه‌ی حاصل، دیگر با اطلاعات درون حافظه‌ای سمت کلاینت، هماهنگ نخواهد بود و کار به روز رسانی دستی آن‌ها به‌عهده‌ی شماست.


بررسی نحوه‌ی عملکرد ExecuteUpdate و ExecuteDelete با یک مثال

فرض کنید مدل‌های موجودیت‌های برنامه شامل کلاس‌های زیر هستند:
public class User
{
    public int Id { get; set; }
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
    public virtual List<Book> Books { get; set; } = new();
    public virtual Address? Address { get; set; }
}

public class Book
{
    public int Id { get; set; }
    public required string Type { get; set; }
    public required string Name { get; set; }

    public virtual User User { get; set; } = default!;
    public int UserId { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public required string Street { get; set; }
    
    public virtual User User { get; set; } = default!;
    public int UserId { get; set; }
}
که در اینجا یک کاربر می‌تواند دارای یک آدرس و چندین کتاب تعریف شده باشد؛ با این Context ابتدایی:
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {
    }

    public DbSet<User> Users { get; set; } = default!;

    public DbSet<Book> Books { get; set; } = default!;

    public DbSet<Address> Addresses { get; set; } = default!;
}

مثال 1: حذف دسته‌ای تعدادی کتاب
context.Books.Where(book => book.Name.Contains("1")).ExecuteDelete();
در اینجا نحوه‌ی استفاده از متد ExecuteDelete را مشاهده می‌کنید که به انتهای LINQ Query، اضافه شده‌است. در این مثال، تمام کتاب‌هایی که در نامشان حرف 1 وجود دارد، حذف می‌شوند. این کوئری، به صورت زیر بر روی بانک اطلاعاتی اجرا می‌شود:
DELETE FROM [b]
FROM [Books] AS [b]
WHERE [b].[Name] LIKE N'%1%'
مهم‌ترین مزیت این روش، عدم نیاز به بارگذاری و یا ساخت درون حافظه‌ای لیست کتاب‌هایی است که قرار است حذف شوند. کل این عملیات در یک رفت و برگشت ساده و سریع انجام می‌شود.

یک نکته: متد ExecuteDelete، تعداد رکوردهای حذف شده را نیز بازگشت می‌دهد.


مثال 2: حذف کاربران و تمام رکوردهای وابسته به آن

فرض کنید می‌خواهیم تعدادی از کاربران را از بانک اطلاعاتی حذف کنیم:
context.Users.Where(user => user.Id <= 500).ExecuteDelete();
اگر این کوئری را با تنظیمات فعلی اجرا کنیم، با خطای زیر متوقف خواهیم شد:
DELETE FROM [u]
FROM [Users] AS [u]
WHERE [u].[Id] <= 500

The DELETE statement conflicted with the REFERENCE constraint "FK_Books_Users_UserId".
The conflict occurred in database "EF7BulkOperations", table "dbo.Books", column 'UserId'.
عنوان می‌کند که یک کاربر، دارای تعدادی کتاب و آدرسی از پیش ثبت شده‌است و نمی‌توان آن‌را بدون حذف وابستگی‌های آن، حذف کرد. اگر کاربری را حذف کنیم، کلید‌های خارجی ذکر شده‌ی در جداولی که این کلید خارجی را به همراه دارند، غیرمعتبر می‌شوند (و این کلید خارجی تعریف شده، نال پذیر هم نیست). برای رفع این مشکل، یا باید ابتدا در طی دستوراتی جداگانه، وابستگی‌های ممکن را حذف کنیم و یا می‌توان تنظیم cascade delete را به نحو زیر به تعریف جداول مرتبط اضافه کرد تا صدور یک دستور delete، به صورت خودکار سبب حذف وابستگی‌های مرتبط نیز شود:
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {
    }

    public DbSet<User> Users { get; set; } = default!;

    public DbSet<Book> Books { get; set; } = default!;

    public DbSet<Address> Addresses { get; set; } = default!;

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

        modelBuilder
            .Entity<User>()
            .HasMany(user => user.Books)
            .WithOne(book => book.User)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder
            .Entity<User>()
            .HasOne(user => user.Address)
            .WithOne(address => address.User)
            .HasForeignKey<Address>(address => address.UserId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}
همانطور که ملاحظه می‌کنید، به متد OnModelCreating تنظیم cascade delete وابستگی‌های جدول کاربران اضافه شده‌است. پس از این تنظیم، دستور مثال دوم، بدون مشکل اجرا شده و حذف یک کاربر، سبب حذف خودکار کتاب‌ها و آدرس او نیز می‌شود.


مثال 3: به‌روز رسانی دسته‌ای از کاربران

فرض کنید می‌خواهیم LastName تعدادی کاربر مشخص را به مقدار جدید Updated، تغییر دهیم:
context.Users.Where(user => user.Id <= 400)
   .ExecuteUpdate(p => p.SetProperty(user => user.LastName,  user => "Updated"));
برای اینکار، پس از مشخص شدن شرط کوئری در قسمت Where، کار به روز رسانی توسط متد ExecuteUpdate و سپس متد SetProperty صورت می‌گیرد. در اینجا در ابتدا مشخص می‌کنیم که کدام خاصیت قرار است به روز رسانی شود و پارامتر دوم آن، مقدار جدید را مشخص می‌کند. این کوئری به نحو زیر به بانک اطلاعاتی اعمال خواهد شد:
UPDATE [u]
SET [u].[LastName] = N'Updated'
FROM [Users] AS [u]
WHERE [u].[Id] <= 400
در اینجا می‌توان در پارامتر دوم متد SetProperty، از مقدار فعلی سایر خواص نیز استفاده کرد:
context.Users.Where(user => user.Id <= 300)
  .ExecuteUpdate(p => p.SetProperty(user => user.LastName,
      user => "Updated" + user.LastName));
که خروجی زیر را تولید می‌کند:
UPDATE [u]
SET [u].[LastName] = N'Updated' + [u].[LastName]
FROM [Users] AS [u]
WHERE [u].[Id] <= 300
همچنین می‌توان چندین متد SetProperty را نیز به صورت زنجیروار، جهت به روز رسانی چندین خاصیت و فیلد، ذکر کرد:
context.Users.Where(user => user.Id <= 800)
   .ExecuteUpdate(p => p.SetProperty(user => user.LastName,
        user => "Updated" + user.LastName)
             .SetProperty(user => user.FirstName,
                 user => "Updated" + user.FirstName));
با این خروجی نهایی:
UPDATE [u]
SET [u].[FirstName] = N'Updated' + [u].[FirstName],
[u].[LastName] = N'Updated' + [u].[LastName]
FROM [Users] AS [u]
WHERE [u].[Id] <= 800
متد ExecuteUpdate، تعداد رکوردهای به‌روز رسانی شده را نیز بازگشت می‌دهد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید:  EF7BulkOperations.zip
مطالب
آشنایی با NHibernate - قسمت سوم

در ادامه، تعاریف سایر موجودیت‌های سیستم ثبت سفارشات و نگاشت آن‌ها را بررسی خواهیم کرد.

کلاس Product تعریف شده در فایل جدید Product.cs در پوشه domain برنامه:

namespace NHSample1.Domain
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal UnitPrice { get; set; }
public bool Discontinued { get; set; }
}
}
کلاس ProductMapping تعریف شده در فایل جدید ProductMapping.cs (توصیه شده است که به ازای هر کلاس یک فایل جداگانه در نظر گرفته شود)، در پوشه Mappings برنامه:

using FluentNHibernate.Mapping;
using NHSample1.Domain;

namespace NHSample1.Mappings
{
public class ProductMapping : ClassMap<Product>
{
public ProductMapping()
{
Not.LazyLoad();
Id(p => p.Id).GeneratedBy.HiLo("1000");
Map(p => p.Name).Length(50).Not.Nullable();
Map(p => p.UnitPrice).Not.Nullable();
Map(p => p.Discontinued).Not.Nullable();
}
}
}
همانطور که ملاحظه می‌کنید، روش تعریف آن‌ها همانند شیء Customer است که در قسمت‌های قبل بررسی شد و نکته جدیدی ندارد.
آزمون واحد بررسی این نگاشت نیز همانند مثال قبلی است.
کلاس ProductMapping_Fixture را در فایل جدید ProductMapping_Fixture.cs به پروژه UnitTests خود (که ارجاعات آن‌را در قسمت قبل مشخص کردیم) خواهیم افزود:

using NUnit.Framework;
using FluentNHibernate.Testing;
using NHSample1.Domain;

namespace UnitTests
{
[TestFixture]
public class ProductMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_product()
{
new PersistenceSpecification<Product>(Session)
.CheckProperty(p => p.Id, 1001)
.CheckProperty(p => p.Name, "Apples")
.CheckProperty(p => p.UnitPrice, 10.45m)
.CheckProperty(p => p.Discontinued, true)
.VerifyTheMappings();
}
}
}
و پس از اجرای این آزمون واحد، عبارات SQL ایی که به صورت خودکار توسط این ORM جهت بررسی عملیات نگاشت صورت خواهند گرفت به صورت زیر می‌باشند:

ProductMapping_Fixture.can_correctly_map_product : Passed
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 10.45, @p2 = True, @p3 = 1001
NHibernate: SELECT product0_.Id as Id1_0_, product0_.Name as Name1_0_, product0_.UnitPrice as UnitPrice1_0_, product0_.Discontinued as Disconti4_1_0_ FROM "Product" product0_ WHERE product0_.Id=@p0;@p0 = 1001

در ادامه تعریف کلاس کارمند، نگاشت و آزمون واحد آن به صورت زیر خواهند بود:

using System;
namespace NHSample1.Domain
{
public class Employee
{
public int Id { set; get; }
public string LastName { get; set; }
public string FirstName { get; set; }
}
}


using NHSample1.Domain;
using FluentNHibernate.Mapping;

namespace NHSample1.Mappings
{
public class EmployeeMapping : ClassMap<Employee>
{
public EmployeeMapping()
{
Not.LazyLoad();
Id(e => e.Id).GeneratedBy.Assigned();
Map(e => e.LastName).Length(50);
Map(e => e.FirstName).Length(50);
}
}
}


using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;

namespace UnitTests
{
[TestFixture]
public class EmployeeMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_employee()
{
new PersistenceSpecification<Employee>(Session)
.CheckProperty(p => p.Id, 1001)
.CheckProperty(p => p.FirstName, "name1")
.CheckProperty(p => p.LastName, "lname1")
.VerifyTheMappings();
}
}
}
خروجی SQL حاصل از موفقیت آزمون واحد آن:

NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Employee" (LastName, FirstName, Id) VALUES (@p0, @p1, @p2);@p0 = 'lname1', @p1 = 'name1', @p2 = 1001
NHibernate: SELECT employee0_.Id as Id4_0_, employee0_.LastName as LastName4_0_, employee0_.FirstName as FirstName4_0_ FROM "Employee" employee0_ WHERE employee0_.Id=@p0;@p0 = 1001

همانطور که ملاحظه می‌کنید، این آزمون‌های واحد 4 مرحله را در یک سطر انجام می‌دهند:
الف) ایجاد یک وهله از کلاس Employee
ب) ثبت اطلاعات کارمند در دیتابیس
ج) دریافت اطلاعات کارمند در وهله‌ای جدید از شیء Employee
د) و در پایان بررسی می‌کند که آیا شیء جدید ایجاد شده با شیء اولیه مطابقت دارد یا خیر

اکنون در ادامه پیاده سازی سیستم ثبت سفارشات، به قسمت جالب این مدل می‌رسیم. قسمتی که در آن ارتباطات اشیاء و روابط one-to-many تعریف خواهند شد. تعاریف کلاس‌های OrderItem و OrderItemMapping را به صورت زیر در نظر بگیرید:

کلاس OrderItem تعریف شده در فایل جدید OrderItem.cs واقع شده در پوشه domain پروژه:
که در آن هر سفارش (order) دقیقا از یک محصول (product) تشکیل می‌شود و هر محصول می‌تواند در سفارشات متعدد و مختلفی درخواست شود.

namespace NHSample1.Domain
{
public class OrderItem
{
public int Id { get; set; }
public int Quantity { get; set; }
public Product Product { get; set; }
}
}
کلاس OrderItemMapping تعریف شده در فایل جدید OrderItemMapping.cs :

using FluentNHibernate.Mapping;
using NHSample1.Domain;

namespace NHSample1.Mappings
{
public class OrderItemMapping : ClassMap<OrderItem>
{
public OrderItemMapping()
{
Not.LazyLoad();
Id(oi => oi.Id).GeneratedBy.Assigned();
Map(oi => oi.Quantity).Not.Nullable();
References(oi => oi.Product).Not.Nullable();
}
}
}
نکته جدیدی که در این کلاس نگاشت مطرح شده است، واژه کلیدی References می‌باشد که جهت بیان این ارجاعات و وابستگی‌ها بکار می‌رود. این ارجاع بیانگر یک رابطه many-to-one بین سفارشات و محصولات است. همچنین در ادامه آن Not.Nullable ذکر شده است تا این ارجاع را اجباری نمائید (در غیر اینصورت سفارش غیر معتبر خواهد بود).
نکته‌ی دیگر مهم آن این مورد است که Id در اینجا به صورت یک کلید تعریف نشده است. یک آیتم سفارش داده شده، موجودیت به حساب نیامده و فقط یک شیء مقداری (value object) است و به خودی خود امکان وجود ندارد. هر وهله از آن تنها توسط یک سفارش قابل تعریف است. بنابراین id در اینجا فقط به عنوان یک index می‌تواند مورد استفاده قرار گیرد و فقط توسط شیء Order زمانیکه یک OrderItem به آن اضافه می‌شود، مقدار دهی خواهد شد.

اگر برای این نگاشت نیز آزمون واحد تهیه کنیم، به صورت زیر خواهد بود:

using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;

namespace UnitTests
{
[TestFixture]
public class OrderItemMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_order_item()
{
var product = new Product
{
Name = "Apples",
UnitPrice = 4.5m,
Discontinued = true
};

new PersistenceSpecification<OrderItem>(Session)
.CheckProperty(p => p.Id, 1)
.CheckProperty(p => p.Quantity, 5)
.CheckReference(p => p.Product, product)
.VerifyTheMappings();
}
}
}

مشکل! این آزمون واحد با شکست مواجه خواهد شد، زیرا هنوز مشخص نکرده‌ایم که دو شیء Product را که در قسمت CheckReference فوق برای این منظور معرفی کرده‌ایم، چگونه باید با هم مقایسه کرد. در مورد مقایسه نوع‌های اولیه و اصلی مانند int و string و امثال آن مشکلی نیست، اما باید منطق مقایسه سایر اشیاء سفارشی خود را با پیاده سازی اینترفیس IEqualityComparer دقیقا مشخص سازیم:

using System.Collections;
using NHSample1.Domain;

namespace UnitTests
{
public class CustomEqualityComparer : IEqualityComparer
{
public bool Equals(object x, object y)
{
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;

if (x is Product && y is Product)
return (x as Product).Id == (y as Product).Id;

if (x is Customer && y is Customer)
return (x as Customer).Id == (y as Customer).Id;

if (x is Employee && y is Employee)
return (x as Employee).Id == (y as Employee).Id;

if (x is OrderItem && y is OrderItem)
return (x as OrderItem).Id == (y as OrderItem).Id;


return x.Equals(y);
}

public int GetHashCode(object obj)
{
//شاید وقتی دیگر
return obj.GetHashCode();
}
}
}
در اینجا فقط Id این اشیاء با هم مقایسه شده است. در صورت نیاز تمامی خاصیت‌های این اشیاء را نیز می‌توان با هم مقایسه کرد (یک سری از اشیاء بکار گرفته شده در این کلاس در ادامه بحث معرفی خواهند شد).
سپس برای بکار گیری این کلاس جدید، سطر مربوط به استفاده از PersistenceSpecification به صورت زیر تغییر خواهد کرد:

new PersistenceSpecification<OrderItem>(Session, new CustomEqualityComparer())

پس از این تغییرات و مشخص سازی نحوه‌ی مقایسه دو شیء سفارشی، آزمون واحد ما پاس شده و خروجی SQL تولید شده آن به صورت زیر می‌باشد:

NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 4.5, @p2 = True, @p3 = 1001
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 5, @p1 = 1001, @p2 = 1
NHibernate: SELECT orderitem0_.Id as Id0_1_, orderitem0_.Quantity as Quantity0_1_, orderitem0_.Product_id as Product3_0_1_, product1_.Id as Id3_0_, product1_.Name as Name3_0_, product1_.UnitPrice as UnitPrice3_0_, product1_.Discontinued as Disconti4_3_0_ FROM "OrderItem" orderitem0_ inner join "Product" product1_ on orderitem0_.Product_id=product1_.Id WHERE orderitem0_.Id=@p0;@p0 = 1

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

using System;
using System.Collections.Generic;

namespace NHSample1.Domain
{
public class Order
{
public int Id { set; get; }
public DateTime OrderDate { get; set; }
public Employee Employee { get; set; }
public Customer Customer { get; set; }
public IList<OrderItem> OrderItems { get; set; }
}
}
نکته‌ی مهمی که در این کلاس وجود دارد استفاده از IList جهت معرفی مجموعه‌ای از آیتم‌های سفارشی است (بجای List و یا IEnumerable که در صورت استفاده خطای type cast exception در حین نگاشت حاصل می‌شد).

using NHSample1.Domain;
using FluentNHibernate.Mapping;

namespace NHSample1.Mappings
{
public class OrderMapping : ClassMap<Order>
{
public OrderMapping()
{
Not.LazyLoad();
Id(o => o.Id).GeneratedBy.GuidComb();
Map(o => o.OrderDate).Not.Nullable();
References(o => o.Employee).Not.Nullable();
References(o => o.Customer).Not.Nullable();
HasMany(o => o.OrderItems)
.AsList(index => index.Column("ListIndex").Type<int>());
}
}
}
در تعاریف نگاشت این کلاس نیز دو ارجاع به اشیاء کارمند و مشتری وجود دارد که با References مشخص شده‌اند.
قسمت جدید آن HasMany است که جهت تعریف رابطه one-to-many بکار گرفته شده است. یک سفارش رابطه many-to-one با یک مشتری و همچنین کارمندی که این رکورد را ثبت می‌کند، دارد. در اینجا مجموعه آیتم‌های یک سفارش به صورت یک لیست بازگشت داده می‌شود و ایندکس آن به ستونی به نام ListIndex در یک جدول دیتابیس نگاشت خواهد شد. نوع این ستون، int می‌باشد.

using System;
using System.Collections.Generic;
using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;

namespace UnitTests
{
[TestFixture]
public class OrderMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_an_order()
{
{
var product1 =
new Product
{
Name = "Apples",
UnitPrice = 4.5m,
Discontinued = true
};
var product2 =
new Product
{
Name = "Pears",
UnitPrice = 3.5m,
Discontinued = false
};

Session.Save(product1);
Session.Save(product2);

var items = new List<OrderItem>
{
new OrderItem
{
Id = 1,
Quantity = 100,
Product = product1
},
new OrderItem
{
Id = 2,
Quantity = 200,
Product = product2
}
};

var customer = new Customer
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};

var employee =
new Employee
{
FirstName = "name1",
LastName = "lname1"
};



var order = new Order
{
Customer = customer,
Employee = employee,
OrderDate = DateTime.Today,
OrderItems = items
};

new PersistenceSpecification<Order>(Session, new CustomEqualityComparer())
.CheckProperty(o => o.OrderDate, order.OrderDate)
.CheckReference(o => o.Customer, order.Customer)
.CheckReference(o => o.Employee, order.Employee)
.CheckList(o => o.OrderItems, order.OrderItems)
.VerifyTheMappings();
}
}
}
}
همانطور که ملاحظه می‌کنید در این متد آزمون واحد، نیاز به مشخص سازی منطق مقایسه اشیاء سفارش، مشتری و آیتم‌های سفارش داده شده نیز وجود دارد که پیشتر در کلاس CustomEqualityComparer معرفی شدند؛ درغیر اینصورت این آزمون واحد با شکست مواجه می‌شد.
متد آزمون واحد فوق کمی طولانی است؛ زیرا در آن باید تعاریف انواع و اقسام اشیاء مورد استفاده را مشخص نمود (و ارزش کار نیز دقیقا در همینجا مشخص می‌شود که بجای SQL نوشتن، با اشیایی که توسط کامپایلر تحت نظر هستند سر و کار داریم).
تنها نکته جدید آن استفاده از CheckList برای بررسی IList تعریف شده در قسمت قبل است.

خروجی SQL این آزمون واحد پس از اجرا و موفقیت آن به صورت زیر است:

OrderMapping_Fixture.can_correctly_map_an_order : Passed
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 3, @p1 = 2
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 4.5, @p2 = True, @p3 = 1001
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Pears', @p1 = 3.5, @p2 = False, @p3 = 1002
NHibernate: INSERT INTO "Customer" (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 2002
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 4, @p1 = 3
NHibernate: INSERT INTO "Employee" (LastName, FirstName, Id) VALUES (@p0, @p1, @p2);@p0 = 'lname1', @p1 = 'name1', @p2 = 3003
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 100, @p1 = 1001, @p2 = 1
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 200, @p1 = 1002, @p2 = 2
NHibernate: INSERT INTO "Order" (OrderDate, Employee_id, Customer_id, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 2009/10/10 12:00:00 ق.ظ, @p1 = 3003, @p2 = 2002, @p3 = 0
NHibernate: UPDATE "OrderItem" SET Order_id = @p0, ListIndex = @p1 WHERE Id = @p2;@p0 = 0, @p1 = 0, @p2 = 1
NHibernate: UPDATE "OrderItem" SET Order_id = @p0, ListIndex = @p1 WHERE Id = @p2;@p0 = 0, @p1 = 1, @p2 = 2
NHibernate: SELECT order0_.Id as Id1_2_, order0_.OrderDate as OrderDate1_2_, order0_.Employee_id as Employee3_1_2_, order0_.Customer_id as Customer4_1_2_, employee1_.Id as Id4_0_, employee1_.LastName as LastName4_0_, employee1_.FirstName as FirstName4_0_, customer2_.Id as Id2_1_, customer2_.FirstName as FirstName2_1_, customer2_.LastName as LastName2_1_, customer2_.AddressLine1 as AddressL4_2_1_, customer2_.AddressLine2 as AddressL5_2_1_, customer2_.PostalCode as PostalCode2_1_, customer2_.City as City2_1_, customer2_.CountryCode as CountryC8_2_1_ FROM "Order" order0_ inner join "Employee" employee1_ on order0_.Employee_id=employee1_.Id inner join "Customer" customer2_ on order0_.Customer_id=customer2_.Id WHERE order0_.Id=@p0;@p0 = 0
NHibernate: SELECT orderitems0_.Order_id as Order4_2_, orderitems0_.Id as Id2_, orderitems0_.ListIndex as ListIndex2_, orderitems0_.Id as Id0_1_, orderitems0_.Quantity as Quantity0_1_, orderitems0_.Product_id as Product3_0_1_, product1_.Id as Id3_0_, product1_.Name as Name3_0_, product1_.UnitPrice as UnitPrice3_0_, product1_.Discontinued as Disconti4_3_0_ FROM "OrderItem" orderitems0_ inner join "Product" product1_ on orderitems0_.Product_id=product1_.Id WHERE orderitems0_.Order_id=@p0;@p0 = 0

تا اینجای کار تعاریف اشیاء ، نگاشت آن‌ها و همچنین بررسی صحت این نگاشت‌ها به پایان می‌رسد.

نکته:
دیتابیس برنامه را جهت آزمون‌های واحد برنامه، از نوع SQLite ساخته شده در حافظه مشخص کردیم. اگر علاقمند باشید که database schema تولید شده توسط NHibernate را مشاهده نمائید، در متد SetupContext کلاس FixtureBase که در قسمت قبل معرفی شد، سطر آخر را به صورت زیر تغییر دهید، تا اسکریپت دیتابیس نیز به صورت خودکار در خروجی اس کیوال آزمون واحد لحاظ شود (پارامتر دوم آن مشخص می‌کند که schema ساخته شده، نمایش داده شود یا خیر):

SessionSource.BuildSchema(Session, true);
پس از این تغییر و انجام مجدد آزمون واحد، اسکریپت دیتابیس ما به صورت زیر خواهد بود (که جهت ایجاد یک دیتابیس SQLite می‌تواند مورد استفاده قرار گیرد):

drop table if exists "OrderItem"

drop table if exists "Order"

drop table if exists "Customer"

drop table if exists "Product"

drop table if exists "Employee"

drop table if exists hibernate_unique_key

create table "OrderItem" (
Id INTEGER not null,
Quantity INTEGER not null,
Product_id INTEGER not null,
Order_id INTEGER,
ListIndex INTEGER,
primary key (Id)
)

create table "Order" (
Id INTEGER not null,
OrderDate DATETIME not null,
Employee_id INTEGER not null,
Customer_id INTEGER not null,
primary key (Id)
)

create table "Customer" (
Id INTEGER not null,
FirstName TEXT not null,
LastName TEXT not null,
AddressLine1 TEXT not null,
AddressLine2 TEXT,
PostalCode TEXT not null,
City TEXT not null,
CountryCode TEXT not null,
primary key (Id)
)

create table "Product" (
Id INTEGER not null,
Name TEXT not null,
UnitPrice NUMERIC not null,
Discontinued INTEGER not null,
primary key (Id)
)

create table "Employee" (
Id INTEGER not null,
LastName TEXT,
FirstName TEXT,
primary key (Id)
)

create table hibernate_unique_key (
next_hi INTEGER
)
البته اگر مستندات SQLite را مطالعه کرده باشید می‌دانید که مفهوم کلید خارجی در این دیتابیس وجود دارد اما اعمال نمی‌شود! (برای اعمال آن باید تریگر نوشت) به همین جهت در این اسکریپت تولیدی خبری از کلید خارجی نیست.

برای اینکه از دیتابیس اس کیوال سرور استفاده کنیم، در همان متد SetupContext کلاس مذکور، سطر اول را به صورت زیر تغییر دهید (نوع دیتابیس اس کیوال سرور 2008 مشخص شده و سپس رشته اتصالی به دیتابیس ذکر گردیده است):

var cfg = Fluently.Configure().Database(
// SQLiteConfiguration.Standard.ShowSql().InMemory
MsSqlConfiguration
.MsSql2008
.ShowSql()
.ConnectionString("Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true")
);

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

if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3EF88858466CFBF7]') AND parent_object_id = OBJECT_ID('[OrderItem]'))
alter table [OrderItem] drop constraint FK3EF88858466CFBF7


if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3EF888589F32DE52]') AND parent_object_id = OBJECT_ID('[OrderItem]'))
alter table [OrderItem] drop constraint FK3EF888589F32DE52


if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3117099B1EBA72BC]') AND parent_object_id = OBJECT_ID('[Order]'))
alter table [Order] drop constraint FK3117099B1EBA72BC


if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3117099BB2F9593A]') AND parent_object_id = OBJECT_ID('[Order]'))
alter table [Order] drop constraint FK3117099BB2F9593A


if exists (select * from dbo.sysobjects where id = object_id(N'[OrderItem]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [OrderItem]

if exists (select * from dbo.sysobjects where id = object_id(N'[Order]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Order]

if exists (select * from dbo.sysobjects where id = object_id(N'[Customer]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Customer]

if exists (select * from dbo.sysobjects where id = object_id(N'[Product]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Product]

if exists (select * from dbo.sysobjects where id = object_id(N'[Employee]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Employee]

if exists (select * from dbo.sysobjects where id = object_id(N'hibernate_unique_key') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table hibernate_unique_key

create table [OrderItem] (
Id INT not null,
Quantity INT not null,
Product_id INT not null,
Order_id INT null,
ListIndex INT null,
primary key (Id)
)

create table [Order] (
Id INT not null,
OrderDate DATETIME not null,
Employee_id INT not null,
Customer_id INT not null,
primary key (Id)
)

create table [Customer] (
Id INT not null,
FirstName NVARCHAR(50) not null,
LastName NVARCHAR(50) not null,
AddressLine1 NVARCHAR(50) not null,
AddressLine2 NVARCHAR(50) null,
PostalCode NVARCHAR(10) not null,
City NVARCHAR(50) not null,
CountryCode NVARCHAR(2) not null,
primary key (Id)
)

create table [Product] (
Id INT not null,
Name NVARCHAR(50) not null,
UnitPrice DECIMAL(19,5) not null,
Discontinued BIT not null,
primary key (Id)
)

create table [Employee] (
Id INT not null,
LastName NVARCHAR(50) null,
FirstName NVARCHAR(50) null,
primary key (Id)
)

alter table [OrderItem]
add constraint FK3EF88858466CFBF7
foreign key (Product_id)
references [Product]

alter table [OrderItem]
add constraint FK3EF888589F32DE52
foreign key (Order_id)
references [Order]

alter table [Order]
add constraint FK3117099B1EBA72BC
foreign key (Employee_id)
references [Employee]

alter table [Order]
add constraint FK3117099BB2F9593A
foreign key (Customer_id)
references [Customer]

create table hibernate_unique_key (
next_hi INT
)
که نکات ذیل در مورد آن جالب توجه است:
الف) جداول مطابق نام کلاس‌های ما تولید شده‌اند.
ب) نام فیلدها دقیقا مطابق نام خواص کلاس‌های ما تشکیل شده‌اند.
ج) Id ها به صورت primary key تعریف شده‌اند (از آنجائیکه ما در هنگام تعریف نگاشت‌ها، آن‌ها را از نوع identity مشخص کرده بودیم).
د) رشته‌ها به نوع nvarchar با اندازه 50 نگاشت شده‌اند.
ه) کلیدهای خارجی بر اساس نام جدول با پسوند _id تشکیل شده‌اند.




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


نظرات مطالب
استفاده‌ی گسترده از DateTimeOffset در NET Core.
یک نکته‌ی تکمیلی: تبدیلگرهای DateTimeOffset برای بانک‌های اطلاعاتی که از آن پشتیبانی نمی‌کنند

خود EF Core به همراه تبدیلگرهای توکار زیر برای کار ساده‌تر با DateTimeOffset در بانک اطلاعاتی‌هایی مانند SQLite و یا MySQL است:

DateTimeOffsetToBinaryConverter - DateTimeOffset to binary-encoded 64-bit value (stores it as a long, slight reduction in precision)

DateTimeOffsetToBytesConverter - DateTimeOffset to byte array (stores it as a 12 byte array, 8 bytes for time, 4 bytes for offset. Full precision.)

DateTimeOffsetToStringConverter - DateTimeOffset to string (ISO 8601 string including timezone) 

و برای مثال می‌توان آن‌ها را به صورت زیر و سراسری، به سیستم معرفی کرد:
protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite")
    {
        // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
        // here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations
        // To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset
        // use the DateTimeOffsetToBinaryConverter
        // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754
        // This only supports millisecond precision, but should be sufficient for most use cases.
        foreach (var entityType in builder.Model.GetEntityTypes())
        {
            var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset));
            foreach (var property in properties)
            {
                builder
                    .Entity(entityType.Name)
                    .Property(property.Name)
                    .HasConversion(new DateTimeOffsetToBinaryConverter());
            }
        }
    }
}
مطالب
استفاده‌ی گسترده از DateTimeOffset در NET Core.
اگر به سورس‌های ASP.NET Identity نگارش‌های 2 و 3 دقت کنیم، این تفاوت به وضوح قابل مشاهده‌است:
در نگارش 2
public virtual DateTime? LockoutEndDateUtc { get; set; }
در نگارش 3
public virtual DateTimeOffset? LockoutEnd { get; set; }
و در کل، در طراحی تمام قسمت‌ها و اجزای NET Core. بجای استفاده‌ی از DateTime متداول، شاهد استفاده‌ی گسترده‌ای از DateTimeOffset هستیم که از زمان ارائه‌ی NET 3.5. معرفی شده‌است. چرا؟


مشکل ساختار DateTime چیست؟

تمام کسانیکه مدتی با NET Framework. کار کرده‌اند، قطعا از ساختار DateTime برای ذخیره سازی اطلاعاتی زمانی محلی استفاده کرد‌ه‌اند. اما مشکل DateTime چیست؟
فرض کنید در حال استفاده‌ی از یک وب سرویس قرار گرفته‌ی در یک منطقه‌ی زمانی غربی هستید و این وب سرویس تاریخ تولد افراد را با یک چنین فرمتی ارائه می‌دهد:
 2012-03-01 00:00:00-05:00
در این حالت برای استفاده‌ی متداول از این زمان می‌توان به صورت زیر عمل کرد:
 var dateString = "2012-03-01 00:00:00-05:00";
var birthDay = DateTime.Parse(dateString);
هرچند این عملیات ساده به نظر می‌رسد، اما با توجه به قرارگیری سرور برنامه در یک منطقه‌ی زمانی دیگر، زمان پردازش شده به صورت ذیل خواهد بود:
 2012-02-29 11:00:00 PM
اتفاقی که رخ داده‌است، تبدیل DateTime رسیده به زمان محلی سرور است و در این حالت تاریخ تولد شخص از یکم ماه، به 29 ام ماه قبل تغییر کرده‌است. علت آن هم وجود 05:00 یا offset (فاصله‌ی با UTC) در تاریخ ارائه شده‌است.
چگونه می‌توان offset را در تاریخ ذکر کرد، اما از تبدیل آن به زمان محلی جلوگیری کرد؟ این مورد جایی‌است که ساختار DateTimeOffset بکار خواهد آمد.


DateTimeOffset و ذخیره‌ی DateTime به همراه Offset

ساختار کلی DateTimeOffset بسیار واضح بوده و تشکیل شده‌است از Date + Time + Offset. اهمیت آن نیز به ذخیره سازی اطلاعات منطقه‌ی زمانی، در قسمت Offset ساختار ارائه شده بر می‌گردد. ساختار DateTimeOffset در بسیاری از موارد با DateTime متداول یکسان است و تفاوت‌های آن شامل خواص اضافی ذیل هستند:
- DateTime: قسمت DateTime مقدار را بدون توجه به offset باز می‌گرداند (به زمان محلی تبدیل نخواهد شد).
- LocalDateTime: قسمت DateTime را با توجه به منطقه زمانی سروری که برنامه بر روی آن اجرا می‌شود، بر می‌گرداند.
- Offset: فاصله‌ی زمانی با UTC را بیان می‌کند. یک TimeSpan است که فاصله‌ی با UTC را بیان می‌کند.
- UtcDateTime: قسمت DateTime را با توجه به UTC time ارائه می‌کند.

در این ساختار خواص Now و UtcNow نیز یک DateTimeOffset را باز می‌گردانند.


چه زمانی از DateTime و چه زمانی از DateTimeOffset استفاده کنیم؟

اگر هدف شما ذخیره سازی اطلاعات زمانی محلی (جایی که سرور برنامه قرار دارد) است، از DateTime استفاده کنید. اما اگر می‌خواهید مقادیر زمانی را در مناطق زمانی دیگری نیز مورد استفاده قرار دهید و علاقمندید که قسمت TimeZone این اطلاعات نیز حفظ شود، از DateTimeOffset استفاده نمائید.

در این حالت روش پردازش صحیح مثال ابتدای بحث به صورت ذیل خواهد بود:
 string birthDay = "2012-03-01 00:00:00-05:00";
var dtOffset = DateTimeOffset.Parse(birthDay);
و در اینجا اگر علاقمند به مقایسه‌ی این مقدار با یک زمان محلی هستیم، می‌توان از خاصیت Date آن استفاده کرد:
 var theDay = dtOffset.Date;
مطابق توصیه‌ی تیم BCL، استفاده از DateTimeOffset روش ترجیح داده شده‌ی برای ذخیره سازی اطلاعات اکثر سناریوهای زمانی است.


SQL Server و پشتیبانی از DateTimeOffset

ساختار داده‌ای datetime در SQL Server نیز اطلاعات منطقه‌ی زمانی را ذخیره نمی‌کند و درصورت بازیابی آن در برنامه، این زمان، به زمان محلی تبدیل خواهد شد. برای رفع این مشکل، از زمان ارائه‌ی SQL Server 2008، ساختار DateTimeOffset نیز به نوع‌های داده‌آی SQL Server اضافه شده‌است:


این ساختار، اطلاعات +00:00 timezone را نیز ذخیره می‌کند.


مشکلات نوع datetime در بانک‌های اطلاعاتی برای ذخیره سازی اطلاعات UTC در آن‌ها

یکی از روش‌های توصیه شده‌ی جهت ذخیره سازی اطلاعات زمانی در بانک‌های اطلاعاتی، استفاد‌ه‌ی از DateTime.UtcNow است. اما زمانیکه از DateTime.UtcNow برای ذخیره سازی اطلاعاتی زمانی استفاده می‌کنیم، به معنای دریافت زمان محلی بر اساس و نسبت به UTC است. در این حالت هنگامیکه آن‌را از یک فیلد datetime بانک اطلاعاتی بازیابی می‌کنیم، از نوع Unspecified خواهد بود (DateTimeKind.Unspecified) و به صورت خودکار به DateTimeKind.Local ترجمه می‌شود. یعنی مقدار آن مجددا به زمان محلی شیفت پیدا خواهد کرد چون نوع datetime بانک اطلاعاتی درکی از DateTimeKind و منطقه‌ی زمانی ندارد.
به همین جهت روش بازیابی صحیح این زمان UTC، نیاز به قید صریح DateTimeKind.Utc را خواهد داشت:
public static class SqlDataReaderExtensions
{
   public static DateTime GetDateTimeUtc(this SqlDataReader reader, string name)
   {
      int fieldOrdinal = reader.GetOrdinal(name);
      DateTime unspecified = reader.GetDateTime(fieldOrdinal);
      return DateTime.SpecifyKind(unspecified, DateTimeKind.Utc);
   }
}
اما اگر نوع فیلد را DateTimeOffset قرار دهیم و از DateTimeOffset.UTCNow برای ذخیره سازی اطلاعات زمانی استفاده کنیم، SqlDataReader بدون نیاز به تبدیلات فوق، قادر است اطلاعات آن‌را به نحو صحیحی دریافت و پردازش کند.


خلاصه‌ی بحث

اگر برنامه‌ی وب شما امروز در یک سرور در اروپا هاست می‌شود و سال بعد در یک سرور کانادایی، استفاده‌ی DateTime.UtcNow کمک زیادی به برنامه نکرده و خروجی SQL Server در این حالت DateTimeKind.Unspecified است و این زمان مجددا بر اساس محل سرور جدید و تنظیمات منطقه‌ی زمانی آن، به حالت DateTimeKind.Local شیفت داده می‌شود که الزاما خروجی صحیحی را به همراه نخواهد داشت و یا اگر قرار است از وب سرویس شما در مناطق زمانی مختلفی استفاده کنند نیز DateTime.UtcNow انتخاب مناسبی نیست. جهت درج فاصله‌ی صحیح با UTC و ذخیره سازی آن در بانک اطلاعاتی، روش توصیه شده، استفاده از نوع DateTimeOffset است و در این حالت دیگر SQL Server اطلاعات را با فرمت زمانی Unspecified بازگشت نمی‌دهد و در سمت کلاینت نیازی به تبدیلات خاصی نخواهد بود.
نظرات مطالب
نمایش تاریخ بر حسب تعداد روزهای گذشته
سلام؛ در قسمتی از سایت، بخش مطالب این ماه قرار داره. شما برای به دست آوردن مطالب این ماه، چطور تاریخ رو محاسبه می‌کنید؟ من خودم به این روش رسیدم:
public class Post
{
       public int Id { get; set; }
       public string Title { get; set; }
       public DateTime dt { get; set; }
}
static void Main(string[] args)
{
       List<Post> ListOfPost = new List<Post>();
       DateTime dt = DateTime.Now;
       PersianCalendar pc = new PersianCalendar();
       int day = pc.GetDayOfMonth(dt);
       int month = pc.GetMonth(dt);
       int year = pc.GetYear(dt);
       int DaysInMonth = pc.GetDaysInMonth(year, month);
       DateTime FirstDayOfCurrentMonth = dt.AddDays(-day).Date;
       DateTime LastDayOfCurrentMonth = 
       FirstDayOfCurrentMonth.AddDays(DaysInMonth);
       var query = ListOfPost
                   .Where(x => x.dt.Date > FirstDayOfCurrentMonth.Date)
                   .Where(x => x.dt.Date <= LastDayOfCurrentMonth.Date)
                   .ToList();
}
این روش بهینه هست ؟
مطالب
بار کردن ساعت و تاریخ فعلی سرور با JQuery Ajax
در این مطلب می‌خواهم شما را با نحوه بار گزاری ساعت و تاریخ سیستم سرور با استفاده از JQuery Ajax آشنا کنم.
در بعضی از سایتها با استفاده از جاوا اسکریپت تاریخ و ساعت جاری سیستم کلاینت به او نشان داه می‌شود.
این روش یک مزیت دارد: اول اینکه این کدها سمت کلاینت اجرا میشن و برای سرور بار اضافی ایجاد نمیکنن.
و یک عیب هم دارد: در صورتی که ساعت و تاریخ روی سیستم کلاینت تنظیم نباشد، همین ساعت و تاریخ نادرست برای او نمایش داده می‌شود. همین عیب می‌تواند باعث افت کیفیت وب سایت شود.

اما راهی هست که تاریخ و ساعت سیستم سرور برای کاربر نشان داده شود و آن هم استفاده از JQuery Ajax هست. به صورتی که هر ثانیه درخواستی برای یک handler فرستاده می‌شود و آن handler نیز ساعت و تاریخ روی سرور را باز می‌گرداند و این مقدار بازگشته شده را می‌توان در تگی از صفحه وب نمایش داد.

مثال: ابتدا یک صفحه aspx می‌سازیم و تگ زیر را در آن قرار می‌دهیم:
<p id="datetime"></p>
ساعت و تاریخ بار شده از سرور در این تگ باید نشان داده شود.

سپس کدهای اسکریپت زیر را می‌نویسیم:
var auto_referesh = setInterval
 (
     function()
    {
         $.post
         (
            "GetDateTime.ashx",
             function (result) 
            {
                $('#datetime').html(result);
            }
        );
     }, 1000
 );
با نوشتن این کدها هر ثانیه یک بار، بوسیله Ajax درخواستی برای یک handler به اسم GetDateTime.ashx فرستاده می‌شود. وظیفه این handler برگرداندن تاریخ و ساعت فعلی سیستم سرور است. بعد از دریافت مقدار این مقدار از این handler، آنرا در تگ با شناسه datetime قرار می‌دهیم.

کد استفاده شده در handler هم به این صورت است:
<%@ WebHandler Language="C#" Class="GetDateTime" %>

using System;
using System.Web;

public class GetDateTime : IHttpHandler {
    
    public void ProcessRequest (HttpContext context) {
        context.Response.ContentType = "text/plain";
        context.Response.Write(DateTime.Now.ToString());
    }
 
    public bool IsReusable {
        get {
            return false;
        }
    }

}
در انتها فایل ضمیمه این مثال را از این لینک دریافت کنید:
AjaxDateTime.zip