مطالب
بررسی دقیق عملکرد AutoMapper
همانطور که اطلاع دارید، AutoMapper ابزاری برای نگاشت خودکار بین Model و Dto می‌باشد؛ که به صورت نادرست تصور کاهش سرعت در استفاده کردن از آن، بین توسعه دهندگان جا افتاده‌است. در این مقاله قصد داریم به صورت دقیق، به بررسی سرعت عملکرد استفاده از AutoMapper و مقایسه آن با نگاشت دستی بپردازیم.
کد‌های کامل این قسمت را میتوانید از اینجا clone کرده و شخصا تست نمایید.

ابتدا یک پروژه‌ی Console Application را ساخته و AutoMapper را به همراه Ef6، نصب مینماییم. سپس دو کلاس جدید را به نام‌های User و Address به صورت زیر در پوشه‌ی Models مینویسیم.
using System.Collections.Generic;

namespace AutoMapperComparison.Models
{
    public class User
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public ICollection<Address> Addresses { get; set; }
    }
}
using System.ComponentModel.DataAnnotations.Schema;

namespace AutoMapperComparison.Models
{
    public class Address
    {
        public int Id { get; set; }

        public double? Code { get; set; }

        public string Title { get; set; }

        public int UserId { get; set; }

        [ForeignKey(nameof(UserId))]
        public virtual User User { get; set; }
    }
}
بدیهی است که این دو مدل با همدیگر رابطه‌ی 1 به چند دارند. حال کافیست AppDbContext خود را به صورت زیر تعریف نماییم.
نکته: در متد Seed، برای ثبت رکورد‌های اولیه، از BulkInsert استفاده شده است (باید پکیج BulkInsert را نیز نصب نمایید)
using EntityFramework.BulkInsert.Extensions;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;

namespace AutoMapperComparison.Models
{
    public class AppDbContextInitializer : DropCreateDatabaseAlways<AppDbContext>
    {
        protected override void Seed(AppDbContext context)
        {
            User user = context.Users.Add(new User { Name = "Test" });

            context.SaveChanges();

            List<Address> addresses = new List<Address>();

            for (int i = 0; i < 500000; i++)
            {
                addresses.Add(new Address { Id = i, Code = 1, Title = "Test", UserId = user.Id });
            }

            context.BulkInsert(addresses);

            base.Seed(context);
        }
    }

    public class AppDbContext : DbContext
    {
        static AppDbContext()
        {
            Database.SetInitializer(new AppDbContextInitializer());

            //Database.SetInitializer<AppDbContext>(null);
        }

        public AppDbContext()
            : base(new SqlConnection(@"Data Source=.;Initial Catalog=AppDbContext;Integrated Security=True"), contextOwnsConnection: true)
        {
            Configuration.AutoDetectChangesEnabled = false;
            Configuration.EnsureTransactionsForFunctionsAndCommands = false;
            Configuration.LazyLoadingEnabled = false;
            Configuration.ProxyCreationEnabled = false;
            Configuration.ValidateOnSaveEnabled = false;
            Configuration.UseDatabaseNullSemantics = false;
        }

        public DbSet<User> Users { get; set; }

        public DbSet<Address> Addresses { get; set; }
    }
}
 برای اینکه مقایسه انجام شده دقیق باشد، تمامی Configuration‌های اضافی را نیز غیر فعال نموده‌ام.
فقط نیاز داریم یک Dto را برای Address نیز تعریف کنیم؛ چون قرار است نگاشت از Model به Dto از روی Address و AddressDto انجام شود.
کلاس AddressDto را به صورت زیر ایجاد میکنیم:
namespace AutoMapperComparison.Models
{
    public class AddressDto
    {
        public int Id { get; set; }

        public double? Code { get; set; }

        public string Title { get; set; }

        public int UserId { get; set; }

        public string UserName { get; set; }
    }
}
قرار است به صورت خودکار از طریق AutoMapper و همچنین به صورت دستی، نگاشت از Model به Dto مربوطه انجام شود.
 حال نیاز است فایل Program.cs را باز کرده و تغییرات زیر را اعمل نماییم:
using AutoMapper;
using AutoMapper.QueryableExtensions;
using AutoMapperComparison.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace AutoMapperComparison
{
    public class Program
    {
        public static void Main()
        {
            Mapper.Initialize(cfg =>
            {
                cfg.CreateMap<Address, AddressDto>();
            });

            Console.WriteLine($"Create Db {DateTimeOffset.UtcNow}");
            using (AppDbContext db = new AppDbContext())
            {
                db.Database.Initialize(force: true);
                db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS"); //Removes all clean buffers from the buffer pool, and columnstore objects from the columnstore object pool
                Console.WriteLine(db.Addresses.ProjectTo<AddressDto>());
                Console.WriteLine(db.Addresses.Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }));
            }

            Console.WriteLine($"Normal Select {DateTimeOffset.UtcNow}");
            using (AppDbContext db = new AppDbContext())
            {
                db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS");
                Stopwatch watch = Stopwatch.StartNew();
                List<AddressDto> addresses = db.Addresses.AsNoTracking().Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }).ToList();
                List<AddressDto> addresses2 = db.Addresses.AsNoTracking().Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }).ToList();
                watch.Stop();
                Console.WriteLine($"{watch.ElapsedMilliseconds} {addresses.Count} {addresses2.Count}");
            }

            Console.WriteLine($"AutoMapper Exec {DateTimeOffset.UtcNow}");
            using (AppDbContext db = new AppDbContext())
            {
                db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS");
                Stopwatch watch = Stopwatch.StartNew();
                List<AddressDto> addresses = db.Addresses.AsNoTracking().ProjectTo<AddressDto>().ToList();
                List<AddressDto> addresses2 = db.Addresses.AsNoTracking().ProjectTo<AddressDto>().ToList();
                watch.Stop();
                Console.WriteLine($"{watch.ElapsedMilliseconds} {addresses.Count} {addresses2.Count}");
            }

            Console.ReadKey();
        }
    }
}
نکته: از دستور DBCC DROPCLEANBUFFERS جهت خالی کردن بافر sql server برای رسیدن به نتیجه‌ی هرچه دقیق‌تر استفاده شده است.
بعد از اجرا کردن، ابتدا بررسی میکنیم که کوئری اجرا شده‌ی دو نوع مختلف، هیچ تفاوتی با هم نداشته باشند.


حال نتایج بدست آمده، در قسمت پایین‌تر آن نمایان میشود:


البته نتیجه‌ی این آزمایش بسته به سخت افزار سیستم شما ممکن است کمی متفاوت باشد.

در سه آزمایش دیگر به صورت متوالی نتیجه‌ی زیر بدست آمد:

Normal   AutoMapper
 2451  2378
 2120  2111
 2202  2124

اگر این مقدار جزئی از تفاوت بین دو نوع مختلف آزمایش را مورد نظر نگیریم، میتوان گفت که هر دو روش نتیجه‌ی کاملا یکسانی خواهند داشت. فقط با استفاده از AutoMapper کد‌های کمتری نوشته شده‌است!

اما دلیل چیست؟ از آنجایی که ProjectTo از Dto به Model انجام شده و Lambda Expressionی که به سمت Entity Framework فرستاده شده‌است با روش Normal کاملا برابر است و بقیه‌ی عملیات توسط EF انجام میشود، با قاطعیت میتوان گفت که هر دو روش ذکر شده از نظر Performance کاملا یکسان خواهند بود.

نکته: البته به این موضوع باید توجه شود که اگر همین آزمایش را بطور مثال با استفاده از یک Listی از رکورد‌های درون Memory ساخته شده توسط خودمان انجام دهیم، آن موقع نتیجه‌ی یکسانی نخواهیم داشت، به دلیل اینکه EFی دیگر وجود نخواهد داشت که مسئولیت بازگشت داده‌ها را بر عهده بگیرد. از آنجائیکه اکثر کارهایی که توقع داریم AutoMapper برای ما انجام دهد، توسط ORM بازگشت داده میشود، پس میتوان گفت نکته‌ی فوق تقریبا در دنیای واقعی رخ نخواهد داد و باعث مشکل نخواهد شد.

مطالب
مدیریت Join در NHibernate 3.0

مباحث eager fetching/loading (واکشی حریصانه) و lazy loading/fetching (واکشی در صورت نیاز، با تاخیر، تنبل) جزو نکات کلیدی کار با ORM های پیشرفته بوده و در صورت عدم اطلاع از آن‌ها و یا استفاده‌ی ناصحیح از هر کدام، باید منتظر از کار افتادن زود هنگام سیستم در زیر بار چند کاربر همزمان بود. به همین جهت تصور اینکه "با استفاده از ORMs دیگر از فراگیری SQL راحت شدیم!" یا اینکه "به من چه که پشت صحنه چه اتفاقی می‌افته!" بسی مهلک و نادرست است!
در ادامه به تفصیل به این موضوع پرداخته خواهد شد.

ابزار مورد نیاز

در این مطلب از برنامه‌ی NHProf استفاده خواهد شد.
اگر مطالب NHibernate این سایت را دنبال کرده باشید، در مورد لاگ کردن SQL تولیدی به اندازه‌ی کافی توضیح داده شده یا حتی یک ماژول جمع و جور هم برای مصارف دم دستی نوشته شده است. این موارد شاید این ایده را به همراه داشته باشند که چقدر خوب می‌شد یک برنامه‌ی جامع‌تر برای این نوع بررسی‌ها تهیه می‌شد. حداقل SQL نهایی فرمت می‌شد (یعنی برنامه باید مجهز به یک SQL Parser تمام عیار باشد که کار چند ماهی هست ...؛ با توجه به اینکه مثلا NHibernate از افزونه‌های SQL ویژه بانک‌های اطلاعاتی مختلف هم پشتیبانی می‌کند، مثلا T-SQL مایکروسافت با یک سری ریزه کاری‌های منحصر به MySQL متفاوت است)، یا پس از فرمت شدن، syntax highlighting به آن اضافه می‌شد، در ادامه مشخص می‌کرد کدام کوئری‌ها سنگین‌تر هستند، کدامیک نشانه‌ی عدم استفاده‌ی صحیح از ORM مورد استفاده است، چه مشکلی دارد و از این موارد.
خوشبختانه این ایده‌ها یا آرزوها با برنامه‌ی NHProf محقق شده است. این برنامه برای استفاده‌ی یک ماه اول آن رایگان است (آدرس ایمیل خود را وارد کنید تا یک فایل مجوز رایگان یک ماهه برای شما ارسال گردد) و پس از یک ماه، باید حداقل 300 دلار هزینه کنید.


واکشی حریصانه و غیرحریصانه چیست؟

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


چگونه واکشی حریصانه و غیرحریصانه را در NHibernate 3.0 تنظیم کنیم؟

در NHibernate اگر تنظیم خاصی را تدارک ندیده و خواص جداول خود را به صورت virtual معرفی کرده باشید، تنظیم پیش فرض دریافت اطلاعات همان lazy loading است. به مثالی در این زمینه توجه بفرمائید:

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


using System.Collections.Generic;
namespace CustomerOrdersSample.Domain
{
public class Customer
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Order> Orders { get; set; }
}
}

using System;
using System.Collections.Generic;
namespace CustomerOrdersSample.Domain
{
public class Order
{
public virtual int Id { get; set; }
public virtual DateTime OrderDate { set; get; }
public virtual Customer Customer { get; set; }
public virtual IList<OrderItem> OrderItems { set; get; }
}
}

namespace CustomerOrdersSample.Domain
{
public class OrderItem
{
public virtual int Id { get; set; }
public virtual Product Product { get; set; }
public virtual int Quntity { get; set; }
public virtual Order Order { set; get; }
}
}

namespace CustomerOrdersSample.Domain
{
public class Product
{
public virtual int Id { set; get; }
public virtual string Name { get; set; }
public virtual decimal UnitPrice { get; set; }
}
}

که جداول متناظر با آن به صورت زیر خواهند بود:
    create table Customers (
CustomerId INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
primary key (CustomerId)
)

create table Orders (
OrderId INT IDENTITY NOT NULL,
OrderDate DATETIME null,
CustomerId INT null,
primary key (OrderId)
)

create table OrderItems (
OrderItemId INT IDENTITY NOT NULL,
Quntity INT null,
ProductId INT null,
OrderId INT null,
primary key (OrderItemId)
)

create table Products (
ProductId INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
UnitPrice NUMERIC(19,5) null,
primary key (ProductId)
)

alter table Orders
add constraint fk_Customer_Order
foreign key (CustomerId)
references Customers

alter table OrderItems
add constraint fk_Product_OrderItem
foreign key (ProductId)
references Products

alter table OrderItems
add constraint fk_Order_OrderItem
foreign key (OrderId)
references Orders

همچنین یک سری اطلاعات آزمایشی زیر را هم در نظر بگیرید: (بانک اطلاعاتی انتخاب شده SQL CE است)

SET IDENTITY_INSERT [Customers] ON;
GO
INSERT INTO [Customers] ([CustomerId],[Name]) VALUES (1,N'Customer1');
GO
SET IDENTITY_INSERT [Customers] OFF;
GO
SET IDENTITY_INSERT [Products] ON;
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (1,N'Product1',1000.00000);
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (2,N'Product2',2000.00000);
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (3,N'Product3',3000.00000);
GO
SET IDENTITY_INSERT [Products] OFF;
GO
SET IDENTITY_INSERT [Orders] ON;
GO
INSERT INTO [Orders] ([OrderId],[OrderDate],[CustomerId]) VALUES (1,{ts '2011-01-07 11:25:20.000'},1);
GO
SET IDENTITY_INSERT [Orders] OFF;
GO
SET IDENTITY_INSERT [OrderItems] ON;
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (1,10,1,1);
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (2,5,2,1);
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (3,20,3,1);
GO
SET IDENTITY_INSERT [OrderItems] OFF;
GO

دریافت اطلاعات :
می‌خواهیم نام کلیه محصولات خریداری شده توسط مشتری‌ها را به همراه نام مشتری و زمان خرید مربوطه، نمایش دهیم (دریافت اطلاعات از 4 جدول بدون join نویسی):

var list = session.QueryOver<Customer>().List();

foreach (var customer in list)
{
foreach (var order in customer.Orders)
{
foreach (var orderItem in order.OrderItems)
{
Console.WriteLine("{0}:{1}:{2}", customer.Name, order.OrderDate, orderItem.Product.Name);
}
}
}

خروجی به صورت زیر خواهد بود:
Customer1:2011/01/07 11:25:20 :Product1
Customer1:2011/01/07 11:25:20 :Product2
Customer1:2011/01/07 11:25:20 :Product3
اما بهتر است نگاهی هم به پشت صحنه عملیات داشته باشیم:



همانطور که مشاهده می‌کنید در اینجا اطلاعات از 4 جدول مختلف دریافت می‌شوند اما ما Join ایی را ننوشته‌ایم. ORM هرجایی که به اطلاعات فیلدهای جداول دیگر نیاز داشته، به صورت مستقیم به آن جدول مراجعه کرده و یک کوئری، حاصل این عملیات خواهد بود (مطابق تصویر جمعا 6 کوئری در پشت صحنه برای نمایش سه سطر خروجی فوق اجرا شده است).
این حالت فقط و فقط با تعداد رکورد کم بهینه است (و به همین دلیل هم تدارک دیده شده است). بنابراین اگر برای مثال قصد نمایش اطلاعات حاصل از 4 جدول فوق را در یک گرید داشته باشیم، بسته به تعداد رکوردها و تعداد کاربران همزمان برنامه (خصوصا در برنامه‌های تحت وب)، بانک اطلاعاتی باید بتواند هزاران هزار کوئری رسیده حاصل از lazy loading را پردازش کند و این یعنی مصرف بیش از حد منابع (IO بالا، مصرف حافظه بالا) به همراه بالا رفتن CPU usage و از کار افتادن زود هنگام سیستم.
کسانی که پیش از این با SQL نویسی خو گرفته‌اند احتمالا الان منابع موجود را در مورد نحوه‌ی نوشتن Join در NHibernate زیر و رو خواهند کرد؛ زیرا پیش از این آموخته‌اند که برای دریافت اطلاعات از دو یا چند جدول مرتبط باید Join نوشت. اما همانطور که پیشتر نیز عنوان شد، اگر با جزئیات کار با NHibernate آشنا شویم، نیازی به Join نویسی نخواهیم داشت. اینکار را خود ORM در پشت صحنه باید و می‌تواند مدیریت کند. اما چگونه؟
در NHibernate 3.0 با معرفی QueryOver که جایگزینی از نوع strongly typed همان ICriteria API قدیمی است، یا با معرفی Query که همان LINQ to NHibernate می‌باشد، متدی به نام Fetch نیز تدارک دیده شده است که استراتژی‌های lazy loading و eager loading را به سادگی توسط آن می‌توان مشخص نمود.

مثال: دریافت اطلاعات با استفاده از QueryOver

var list = session
.QueryOver<Customer>()
.Fetch(c => c.Orders).Eager
.Fetch(c => c.Orders.First().OrderItems).Eager
.Fetch(c => c.Orders.First().OrderItems.First().Product).Eager
.List();

foreach (var customer in list)
{
foreach (var order in customer.Orders)
{
foreach (var orderItem in order.OrderItems)
{
Console.WriteLine("{0}:{1}:{2}", customer.Name, order.OrderDate, orderItem.Product.Name);
}
}
}

پشت صحنه:



اینبار فقط یک کوئری حاصل عملیات بوده و join ها به صورت خودکار با توجه به متدهای Fetch ذکر شده که حالت eager loading آن‌ها صریحا مشخص شده است، تشکیل شده‌اند (6 بار رفت و برگشت به بانک اطلاعاتی به یکبار تقلیل یافت).

نکته 1: نتایج تکراری
اگر حاصل join آخر را نمایش دهیم، نتایجی تکراری خواهیم داشت که مربوط است به مقدار دهی customer با سه وهله از شیء مربوطه تا بتواند واکشی حریصانه‌ی مجموعه اشیاء فرزند آن‌را نیز پوشش دهد. برای رفع این مشکل یک سطر TransformUsing باید اضافه شود:
...
.TransformUsing(NHibernate.Transform.Transformers.DistinctRootEntity)
.List();


دریافت اطلاعات با استفاده از LINQ to NHibernate3.0
برای اینکه بتوان متدهای Fetch ذکر شده را به LINQ to NHibernate 3.0 اعمال نمود، ذکر فضای نام NHibernate.Linq ضروری است. پس از آن خواهیم داشت:
var list = session
.Query()
.FetchMany(c => c.Orders)
.ThenFetchMany(o => o.OrderItems)
.ThenFetch(p => p.Product)
.ToList();

اینبار از FetchMany، سپس ThenFetchMany (برای واکشی حریصانه مجموعه‌های فرزند) و در آخر از ThenFetch استفاده خواهد شد.

همانطور که ملاحظه می‌کنید حاصل این کوئری، با کوئری قبلی ذکر شده یکسان است. هر دو، اطلاعات مورد نیاز از دو جدول مختلف را نمایش می‌دهند. اما یکی در پشت صحنه شامل چندین و چند کوئری برای دریافت اطلاعات است، اما دیگری تنها از یک کوئری Join دار تشکیل شده است.


نکته 2: خطاهای ممکن
ممکن است حین تعریف متدهای Fetch در زمان اجرا به خطاهای Antlr.Runtime.MismatchedTreeNodeException و یا Specified method is not supported و یا موارد مشابهی برخورد نمائید. تنها کاری که باید انجام داد جابجا کردن مکان بکارگیری extension methods است. برای مثال متد Fetch باید پس از Where در حالت استفاده از LINQ ذکر شود و نه قبل از آن.

مطالب
API Versioning in ASP.NET Core
در مطالب گذشته، درباره‌ی پیاده سازی API Versioning در ASP.NET Web API و الزامات استفاده‌ی از آن، صحبت شده‌است. اگر مطلب ذکر شده را مطالعه کنید، می‌بینید که پیاده سازی Versioning در ASP.NET Web API کاری دشوار و زمانبر بود؛ اما در ASP.NET Core انجام تمامی آن مراحل، در 1 خط صورت می‌گیرد که در ادامه آن را بررسی میکنیم.

برای شروع با اجرای این دستور در Package Manager Console، پکیج Microsoft.AspNetCore.Mvc.Versioning را داخل پروژه نصب می‌کنیم:
Install-Package Microsoft.AspNetCore.Mvc.Versioning
بعد از نصب، کافیست کد زیر را داخل متد ConfigureServices در فایل Startup.cs پروژه‌ی خود اضافه کنید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddApiVersioning();
    // ...
}
در ابتدا بعد از نصب این پکیج، ممکن است شما API هایی داشته باشید که برای آن‌ها از قبل ورژنی مشخص نکرده باشید (بصورت explicit ). می‌توانید یک Version پیش‌فرض را به برنامه اضافه کرده و برای Endpoint هایی که ورژن ندارند، از آن استفاده کنید :  
services.AddApiVersioning(opt =>
{
    opt.AssumeDefaultVersionWhenUnspecified = true;
    opt.DefaultApiVersion = new ApiVersion(1, 0);
});
در این صورت، API شما به شکل زیر قابل دسترسی خواهد بود:
  • api/foo?api-version=1.0/

پارامتر DefaultApiVersion را برابر با یک ApiVersion قرار داده‌ایم. کلاس ApiVersion دارای Overload‌های مختلفی است. Overload ای را که ما در اینجا از آن استفاده کرده‌ایم، بعنوان پارامتر اول Major Version و برای پارامتر دوم، Minor Version را میگیرد. همچنین بجای Major و Minor میتوان از یک DateTime بعنوان ورژن استفاده کرد:
opt.DefaultApiVersion = new ApiVersion(new DateTime(2018, 10, 22));
و در این صورت API شما به شکل زیر قابل دسترسی می‌باشد: 
  • api/foo?api-version=2018-10-22/

URL Path Segment Versioning

تا به اینجا API Versioning ما بر اساس Query String Parameters انجام می‌شود؛ اما اگر بخواهیم بجای آن به شکل مقابل به API‌‌های خود دسترسی داشته باشیم چطور؟ : api/v1/foo/
برای پیاده سازی به این صورت، کافیست Route کنترلر خود را به این شکل تغییر دهید:
[Route("api/v{version:apiVersion}/[controller]")]
public class FooController : ControllerBase
{
    public ActionResult<IEnumerable<string>> Get()
    {
        return new[] { "value1", "value2" };
    }
}

Header Versioning
 روش سوم انجام Versioning، استفاده از Header است. برای فعال کردن Header Versioning، داخل Startup، کد خود را به شکل زیر تغییر دهید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddApiVersioning(opt => opt.ApiVersionReader = new HeaderApiVersionReader("api-version"));
}
با انجام این تغییر، برای تست API خود دیگر نمی‌توانید از Browser استفاده کنید که این یکی از مشکلات این روش است. برای تست کردن یک درخواست GET ساده مجبور به استفاده از ابزارهایی همچون Postman, CURL و ... هستید. ما در اینجا برای تست از Postman استفاده می‌کنیم:


Deprecating
ممکن است بخواهید یک ورژن را منسوخ دانسته و آن را Deprecate کنید. دقت کنید که Deprecate کردن یک API، به معنی از کار افتادن آن نیست. به این صورت میتوانید یک Endpoint از برنامه خود را Deprecate شده «معرفی» کنید:
[ApiVersion("2")]
[ApiVersion("1", Deprecated = true)]
[Route("api/v{version:apiVersion}/[controller]")]
public class FooController : ControllerBase
{
    [HttpGet]
    public string Get() => "I'm deprecated, Bye bye :(";

    [HttpGet, MapToApiVersion("2.0")]
    public string GetV2() => "Hello world ! :D";
}  
برای برگرداندن نام API‌ها و وضعیت Support شان داخل Response Header، باید ReportApiVersions فعال شود:
services.AddApiVersioning(opt =>
{
    opt.DefaultApiVersion = new ApiVersion(1, 0);
    opt.AssumeDefaultVersionWhenUnspecified = true;
    opt.ReportApiVersions = true;
});
که در نتیجه‌ی آن، Response Header برگشتی به این شکل خواهد بود :


Ignoring Versioning
اگر داخل برنامه‌ی خود، کنترلری را دارید که در طی زمان آپدیت نشده و تغییر نخواهد کرد، می‌توانید از Version زدن آن با استفاده از ApiVersionNeutral جلوگیری کنید:
[ApiVersionNeutral]
[Route("api/[controller]")]
public class BarController : ControllerBase
{
    public string Get() => HttpContext.GetRequestedApiVersion().ToString();
}
اجرای این متد در صورت غیرفعال بودن AssumeDefaultVersionWhenUnspecified باعث وقوع خطای NullReferenceException می‌شود و بدین معناست که همانطور که انتظار داشتیم، Version ای برای این Endpoint تنظیم نشده است.

مطلب تکمیلی:
برای آپدیت کردن و یا معرفی نسخه‌ی جدیدی از یک کنترلر با ورژنی متفاوت، نیازی به Rename کردن کلاس قبلی برای رفع Conflict با نام فایل جدید نیست؛ با استفاده از namespace‌ها میتوانید کنترلری همنام، اما با ورژن و عملکردی متفاوت داشته باشید:
namespace TestVersioning.Controllers.V1
{
    [ApiVersion("1", Deprecated = true)]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class FooController : ControllerBase
    {
        public string Get() => "I'm deprecated, Bye bye :(";
    }
}

namespace TestVersioning.Controllers.V2
{
    [ApiVersion("2")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class FooController : ControllerBase
    {
        public string GetV2() => "Hello world ! :D";
    }
}

مطالب دوره‌ها
ایجاد یک کلاس جدید پویا و وهله‌ای از آن در زمان اجرا توسط Reflection.Emit
توانایی‌های Reflection.Emit صرفا به ایجاد متدهایی کاملا جدید و پویا در زمان اجرا محدود نمی‌شود. برای نمونه کلاس ذیل را درنظر بگیرید:
    public class Person
    {
        private string _name;
        public string Name
        {
            get { return _name; }
        }

        public Person(string name)
        {
            _name = name;
        }
    }
در ادامه قصد داریم معادل این کلاس را به همراه وهله‌ای از آن، به صورتی کاملا پویا در زمان اجرا ایجاد کرده (تصور کنید این کلاس در برنامه وجود خارجی نداشته و تنها جهت درک بهتر کدهای IL ادامه بحث، معرفی گردیده است) و سپس مقداری را به سازنده آن ارسال کنیم.
کدهای کامل و توضیحات این typeBuilder را در ادامه ملاحظه می‌کنید:
using System;
using System.Reflection;
using System.Reflection.Emit;

namespace FastReflectionTests
{
    class Program
    {
        static void Main(string[] args)
        {
            //اسمبلی محل قرارگیری کدهای پویای نهایی در اینجا تعیین می‌شود
            //حالت دسترسی به آن اجرایی درنظر گرفته شده، امکان تعیین حالت‌های دیگری مانند ذخیره سازی نیز وجود دارد
            var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
                                      name: new AssemblyName("Demo"), access: AssemblyBuilderAccess.Run);

            // اکنون داخل این اسمبلی یک ماژول جدید را برای قرار دادن کلاس جدید خود تعریف می‌کنیم
            var moduleBuilder = assemblyBuilder.DefineDynamicModule(name: "PersonModule");

            // کار ساخت نوع و کلاس جدید شخص عمومی از اینجا شروع می‌شود
            var typeBuilder = moduleBuilder.DefineType(name: "Person", attr: TypeAttributes.Public);

            // افزودن فیلد خصوصی نام تعریف شده در سطح کلاس شخص
            var nameField = typeBuilder.DefineField(fieldName: "_name",
                                                    type: typeof(string),
                                                    attributes: FieldAttributes.Private);

            // تعریف سازنده عمومی کلاس شخص که دارای یک آرگومان رشته‌ای است
            var ctor = typeBuilder.DefineConstructor(
                                    attributes: MethodAttributes.Public,
                                    callingConvention: CallingConventions.Standard,
                                    parameterTypes: new[] { typeof(string) });
            // تعریف بدنه سازنده کلاس شخص
            // در اینجا فیلد خصوصی تعریف شده در سطح کلاس باید مقدار دهی شود
            var ctorIL = ctor.GetILGenerator();
            // نکته‌ای در مورد سازنده‌ها
            ctorIL.Emit(OpCodes.Ldarg_0); // اندیس صفر در سازنده کلاس به وهله‌ای از کلاس جاری اشاره می‌کند
            ctorIL.Emit(OpCodes.Ldarg_1); // بارگذاری آرگومان سازنده و قرار دادن آن روی پشته
            // مقدار دهی فیلد خصوصی نام که به وهله‌ای از کلاس جاری و مقدار آرگومان دریافتی نیاز دارد
            ctorIL.Emit(OpCodes.Stfld, nameField);
            ctorIL.Emit(OpCodes.Ret); // پایان کار سازنده

            // تعریف خاصیت رشته‌ای نام در کلاس شخص
            var nameProperty = typeBuilder.DefineProperty(
                                                name: "Name",
                                                attributes: PropertyAttributes.HasDefault,
                                                returnType: typeof(string),
                                                parameterTypes: null); // خاصیت پارامتر ورودی ندارد

            var namePropertyGetMethod = typeBuilder.DefineMethod(
                                                name: "get_Name",
                                                attributes: MethodAttributes.Public |
                //متد ویژه‌ای است که توسط کامپایلر پردازش و تشخیص داده می‌شود
                                                            MethodAttributes.SpecialName |
                                                            MethodAttributes.HideBySig,
                                                returnType: typeof(string),
                                                parameterTypes: Type.EmptyTypes);
            // اتصال گت متد به خاصیت رشته‌ای نام که پیشتر تعریف شد
            nameProperty.SetGetMethod(namePropertyGetMethod);

            // بدنه گت متد در اینجا تعریف خواهد شد
            var namePropertyGetMethodIL = namePropertyGetMethod.GetILGenerator();
            namePropertyGetMethodIL.Emit(OpCodes.Ldarg_0); // بارگذاری اشاره‌گری به وهله‌ای از کلاس جاری در پشته
            namePropertyGetMethodIL.Emit(OpCodes.Ldfld, nameField); // بارگذاری فیلد نام
            namePropertyGetMethodIL.Emit(OpCodes.Ret);

            var t = typeBuilder.CreateType(); // نهایی سازی کار ایجاد نوع جدید

            // ایجاد وهله‌ای از نوع جدید که پارامتری رشته‌ای به سازنده آن ارسال می‌شود
            var instance = Activator.CreateInstance(t, "Vahid");

            // دسترسی به خاصیت نام
            var nProperty = t.GetProperty("Name");
            // و دریافت مقدار آن برای نمایش
            var result = nProperty.GetValue(instance, null);

            Console.WriteLine(result);
        }
    }
}
در اینجا ایجاد یک کلاس جدید با ایجاد یک TypeBuilder واقع در فضای نام  System.Reflection.Emit آغاز می‌شود. پیش از آن نیاز است یک اسمبلی پویا و ماژولی در آن‌را برای قرار دادن کدهای پویای این TypeBuilder ایجاد کنیم. توضیحات مرتبط با دستورات مختلف را به صورت کامنت در کدهای فوق ملاحظه می‌کنید. با استفاده از TypeBuilder و متد DefineField آن می‌توان یک فیلد در سطح کلاس ایجاد کرد و یا توسط متد DefineConstructor آن، سازنده کلاس را با امضایی ویژه تعریف نمود و سپس با دسترسی به ILGenerator آن، بدنه این سازنده را همانند متدهای پویا ایجاد کرد.
اگر به کدهای فوق دقت کرده باشید، متد get_Name به خاصیت Name انتساب داده شده است. علت را در قسمت معرفی اجمالی Reflection زمانیکه لیست متدهای کلاس Person را نمایش دادیم، ملاحظه کرده‌اید. تمام خواص Auto implemented در دات نت، هر چند ظاهر ساده‌ای دارند اما در عمل به دو متد get_Name و set_Name در کدهای IL توسط کامپایلر تبدیل می‌شوند. به همین جهت در اینجا نیاز بود تا get_Name را نیز تعریف کنیم.


چند مثال تکمیلی
Populating a PropertyGrid using Reflection.Emit
Dynamically adding RaisePropertyChanged to MVVM Light ViewModels using Reflection.Emit
مطالب
معرفی و روش استفاده از Dispatcher در WPF
باید این سوال را از خودمان بپرسیم که اصلا چه نیازی به استفاده از Dispatcher در WPF می‌باشد و این که ما چه نیازی داریم با ساختمان Thread آشنا شویم؟

می‌دانید که در پروژه و نرم افزارهایی که توسعه داده می‌شوند بعضی مواقع مشاهده می‌کنیم قسمتی از برنامه نیاز به زمان یا پردازش بیشتری دارد تا عملیات خود را به اتمام برساند و رابط کاربری (User Interface) برنامه در این حین منتظر می‌ماند و یا به اصطلاح (Freeze) می‌شود تا یک پردازش طولانی به اتمام برسد و بعد رابط کاربری به کار خود ادامه می‌دهد و بعضی مواقع پنجره Windows explorer is not responding را مشاهده کرده‌اید که با کلیک بر روی Close the program از آن گذر می‌کنیم. در حالی که برنامه ما باید حالت Responsive  داشته باشد و نباید برنامه با یک‌چنین مواردی روبه رو شود.

ساختمان Thread :
تمامی اشیاء (Objects) در این مدل به دو گروه تقسیم بندی می‌شوند:
Single-Threaded Apartment
Multi-Threaded Apartment
ساختمان  Single-Threaded Apartment (STA) :

STA شامل فقط یک Thread می‌باشد و تمامی اشیاء در این ساختمان فقط می‌توانند متدی را که در این Thread صدا زده می‌شود دریافت کنند؛ یا به عبارتی دیگر هنگامیکه شیء به Thread ای متصل می‌شود دیگر شما قادر نخواهید بود اشیاء UI را به صورت مستقیم و یا از طریق Thread‌های دیگر تغییر دهید. لازم به ذکر است WPF از مدل STA پشتیبانی می‌کند که شامل نکات  زیر می‌باشد:
1. یک Thread در کل برنامه اجرا می‌شود و شامل همه Object‌های WPF می‌باشد.
2. عناصر یا المان‌های  WPF، قابلیت تنظیم Thread Affinity را دارند. منظور این می‌باشدکه Thread‌ها نمی‌توانند با یکدیگر ارتباط  برقرار کنند.
3. اشیاء WPF که قابلیت Thread Affinityرا دارند، از یک Object Dispatcher مشتق می‌شوند.
4. اشیاء WPF متعلق به Thread ای می‌باشند که توسط آن ایجاد شده است و Thread دیگر قادر به دسترسی مستقیم به این اشیاء نمی‌باشد.

ساختمان Multi-Threaded Apartment (MTA) :

MTA  شامل یک یا چندین Thread می‌باشد. تمامی اشیاء در این مدل می‌توانند از هر Thread ای فراخوانی شوند. در حقیقت رجیستر‌های CPU و Ram که در اختیار برنامه قرار داده شده تکه تکه می‌شوند و برنامه ما تعداد Thread‌های مورد نظر را اجرا می‌کند و یکی از راه حل‌های بر طرف کردن اینکه رابط کاربری ما در حالت انتظار باقی نماند استفاده از پردازش کارها بصورت غیر همزمان (Asynchronous) می‌باشد که در اصطلاح Multithreading نامیده می‌شود. 

WPF Dispatcher :

WPF از مدل STA پشتیبانی می‌کند. زمانیکه برنامه WPF  اجرا می‌شود، به طور خودکار یک Dispatcher Object ساخته می‌شود و متد Run صدا زده می‌شود و از آن برای آماده سازی صف پیام‌ها استفاده می‌شود که مدیریت یک صف از کارها بر عهده آن است و کارهای UI را در یک صف FIFO اجرا می‌کند. WPF یک Dispatcher را برای UI Thread ایجاد می‌کند. بنابراین شما نمی‌توانید یک dispatcher دیگر برای آن تعریف کنید. 

نکته : دیاگرام زیر نمایش می‌هد که تمامی اشیاء WPF از DispatcherObject مشتق شده‌اند.

نکته : زمانیکه برنامه WPF  اجرا می‌شود دو Thread ساخته می‌شود:
1.  UI Thread (Main Thread) 
2.  Render Thread
 

UI Thread : مسئولیت تمامی ورودی‌های کاربر، handle events, paints screen و اجرای کدهای برنامه را بر عهده دارد.

Render Thread : در Background اجرا می‌شود و برای Render صفحه نمایش WPF استفاده می‌شود.

نکته: همانطور که آشنا شدیم WPF نمی‌تواند UI Thread را از طریق یک Thread دیگر به روز رسانی کند و یا به عبارتی دیگر یک Thread نمی‌تواند بصورت مستقیم به اشیایی که توسط Thread دیگر ایجاد شده، دسترسی داشته باشد.

Dispatcher برای این کار دو متد را در اختیار ما قرار می‌دهد :

متد Invoke : یک Action یا Delegate را میگیرد و متد آن‌را به صورت همزمان اجرا می‌کند. این مورد به این معنا است که تا زمانی که اجرای متد کامل نگردد، عملیاتی صورت نخواهد گرفت و یا به عبارتی دیگر فراخوان را تا زمانیکه زمانبندی به پایان برسد، در حالت مسدود نگهداری خواهد کرد.

مثال :

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Task.Factory.StartNew(() =>
            {
                InvokeMethodExample();
            });
    }
 
    private void InvokeMethodExample()
    {
        Thread.Sleep(2000);
        Dispatcher.Invoke(() =>
            {
                btn1.Content = "By Invoke";
            });
    }
}
متد BeginInvoke : یک Delegate را می‌گیرد و متد آن‌را به صورت ناهمزمان اجرا می‌کند. این مورد به این معنا است که قبل از آنکه متد فراخوانی گردد، برگشت داده می‌شود و یا به عبارتی دیگر به ما اجازه می‌دهد تا Thread جاری را با کنترل دوباره به جریان بیندازیم.
مثال :
public MainWindow()
{
    InitializeComponent();
    Task.Factory.StartNew(() =>
        {
            BeginInvokeExample();
        });
}
 
private void BeginInvokeExample()
{
    DispatcherOperation op = Dispatcher.BeginInvoke((Action)(() => {
         btn1.Content = "By BeginInvoke";
        }));
}
BeginInvoke یک شیء DispatcherOperation برگشت می‌دهد. این شیء یا Object برای دانستن اینکه وضعیت عملیات کامل شده است یا خیر می‌تواند استفاده شود و همچین دو رویداد Aborted و Completed را فراهم می‌کند. 

مطالب
استفاده از نگارش سوم Google Analytics API در سرویس‌های ویندوز یا برنامه‌های وب
در زمان نگارش این مطلب، آخرین نگارش API مخصوص Google Analytics، نگارش سوم آن است و ... کار کردن با آن دارای مراحل خاصی است که حتما باید رعایت شوند. در غیر اینصورت عملا در یک برنامه‌ی وب یا سرویس ویندوز قابل اجرا نخواهند بود. زیرا در حالت متداول کار با API مخصوص Google Analytics، ابتدا یک صفحه‌ی لاگین به Gmail باز می‌شود که باید به صورت اجباری، مراحل آن را انجام داد تا مشخصات تائید شده‌ی اکانت در حال استفاده‌ی از API، در پوشه‌ی AppData ویندوز برای استفاده‌های بعدی ذخیره شود. این مورد برای یک برنامه‌ی دسکتاپ معمولی مشکل ساز نیست؛ زیرا کاربر برنامه، به سادگی می‌تواند صفحه‌ی مرورگری را که باز شده‌است، دنبال کرده و به اکانت گوگل خود وارد شود. اما این مراحل را نمی‌توان در یک برنامه‌ی وب یا سرویس ویندوز پیگیری کرد، زیرا عموما امکان لاگین از راه دور به سرور و مدیریت صفحه‌ی لاگین به Gmail وجود ندارد یا بهتر است عنوان شود، بی‌معنا است. برای حل این مشکل، گوگل راه حل دیگری را تحت عنوان اکانت‌های سرویس، ارائه داده است که پس از ایجاد آن، یک یک فایل X509 Certificate برای اعتبارسنجی سرویس، در اختیار برنامه نویس قرار می‌گیرد تا بدون نیاز به لاگین دستی به Gmail، بتواند از API گوگل استفاده کند. در ادامه نحوه‌ی فعال سازی این قابلیت و استفاده از آن‌را بررسی خواهیم کرد.


ثبت برنامه‌ی خود در گوگل و انجام تنظیمات آن

اولین کاری که برای استفاده از نگارش سوم Google Analytics API باید صورت گیرد، ثبت برنامه‌ی خود در Google Developer Console است. برای این منظور ابتدا به آدرس ذیل وارد شوید:
سپس بر روی دکمه‌ی Create project کلیک کنید. نام دلخواهی را وارد کرده و در ادامه بر روی دکمه‌ی Create کلیک نمائید تا پروفایل این پروژه ایجاد شود.



تنها نکته‌ی مهم این قسمت، بخاطر سپردن نام پروژه است. زیرا از آن جهت اتصال به API گوگل استفاده خواهد شد.
پس از ایجاد پروژه، به صفحه‌ی آن وارد شوید و از منوی سمت چپ صفحه، گزینه‌ی Credentials را انتخاب کنید. در ادامه در صفحه‌ی باز شده، بر روی دکمه‌ی Create new client id کلیک نمائید.


در صفحه‌ی باز شده، گزینه‌ی Service account را انتخاب کنید. اگر سایر گزینه‌ها را انتخاب نمائید، کاربری که قرار است از API استفاده کند، باید بتواند توسط مرورگر نصب شده‌ی بر روی کامپیوتر اجرا کننده‌ی برنامه، یکبار به گوگل لاگین نماید که این مورد مطلوب برنامه‌های وب و همچنین سرویس‌ها نیست.


در اینجا ابتدا یک فایل مجوز p12 را به صورت خودکار دریافت خواهید کرد و همچنین پس از ایجاد client id، نیاز است، ایمیل آن‌را جایی یادداشت نمائید:


از این ایمیل و همچنین فایل p12 ارائه شده، جهت لاگین به سرور استفاده خواهد شد.
همچنین نیاز است تا به برگه‌ی APIs پروژه‌ی ایجاد شده رجوع کرد و گزینه‌ی Analytics API آن‌را فعال نمود:


تا اینجا کار ثبت و فعال سازی برنامه‌ی خود در گوگل به پایان می‌رسد.


دادن دسترسی به Client ID ثبت شده در برنامه‌ی Google Analytics

پس از اینکه Client ID سرویس خود را ثبت کردید، نیاز است به اکانت Google Analytics خود وارد شوید. سپس در منوی آن، گزینه‌ی Admin را پیدا کرده و به آن قسمت، وارد شوید:


در ادامه به گزینه‌ی User management آن وارد شده و به ایمیل Client ID ایجاد شده در قسمت قبل، دسترسی خواندن و آنالیز را اعطاء کنید:


در صورت عدم رعایت این مساله، کلاینت API، قادر به دسترسی به Google Analytics نخواهد بود.


استفاده از نگارش سوم Google Analytics API در دات نت

قسمت مهم کار، تنظیمات فوق است که در صورت عدم رعایت آن‌ها، شاید نصف روزی را مشغول به دیباگ برنامه شوید. در ادامه نیاز است پیشنیازهای دسترسی به نگارش سوم Google Analytics API را نصب کنیم. برای این منظور، سه بسته‌ی نیوگت ذیل را توسط کنسول پاورشل نیوگت، به برنامه اضافه کنید:
 PM> Install-Package Google.Apis
PM> Install-Package Google.Apis.auth
PM> Install-Package Google.Apis.Analytics.v3
پس از نصب، کلاس GoogleAnalyticsApiV3 زیر، جزئیات دسترسی به Google Analytics API را کپسوله می‌کند:
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Google.Apis.Analytics.v3;
using Google.Apis.Analytics.v3.Data;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;

namespace GoogleAnalyticsAPIv3Tests
{
    public class AnalyticsQueryParameters
    {
        public DateTime Start { set; get; }
        public DateTime End { set; get; } 
        public string Dimensions { set; get; } 
        public string Filters { set; get; }
        public string Metrics { set; get; }
    }

    public class AnalyticsAuthentication
    {
        public Uri SiteUrl { set; get; }
        public string ApplicationName { set; get; }
        public string ServiceAccountEmail { set; get; }
        public string KeyFilePath { set; get; }
        public string KeyFilePassword { set; get; }

        public AnalyticsAuthentication()
        {
            KeyFilePassword = "notasecret";
        }
    }

    public class GoogleAnalyticsApiV3
    {
        public AnalyticsAuthentication Authentication { set; get; }
        public AnalyticsQueryParameters QueryParameters { set; get; }

        public GaData GetData()
        {
            var service = createAnalyticsService();
            var profile = getProfile(service);
            var query = service.Data.Ga.Get("ga:" + profile.Id,
                                QueryParameters.Start.ToString("yyyy-MM-dd"),
                                QueryParameters.End.ToString("yyyy-MM-dd"),
                                QueryParameters.Metrics);
            query.Dimensions = QueryParameters.Dimensions;
            query.Filters = QueryParameters.Filters;
            query.SamplingLevel = DataResource.GaResource.GetRequest.SamplingLevelEnum.HIGHERPRECISION;
            return query.Execute();
        }

        private AnalyticsService createAnalyticsService()
        {
            var certificate = new X509Certificate2(Authentication.KeyFilePath, Authentication.KeyFilePassword, X509KeyStorageFlags.Exportable);
            var credential = new ServiceAccountCredential(
                new ServiceAccountCredential.Initializer(Authentication.ServiceAccountEmail)
                {
                    Scopes = new[] { AnalyticsService.Scope.AnalyticsReadonly }
                }.FromCertificate(certificate));

            return new AnalyticsService(new BaseClientService.Initializer
            {
                HttpClientInitializer = credential,
                ApplicationName = Authentication.ApplicationName
            });
        }

        private Profile getProfile(AnalyticsService service)
        {
            var accountListRequest = service.Management.Accounts.List();
            var accountList = accountListRequest.Execute();
            var site = Authentication.SiteUrl.Host.ToLowerInvariant();
            var account = accountList.Items.FirstOrDefault(x => x.Name.ToLowerInvariant().Contains(site));
            var webPropertyListRequest = service.Management.Webproperties.List(account.Id);
            var webPropertyList = webPropertyListRequest.Execute();
            var sitePropertyList = webPropertyList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site));
            var profileListRequest = service.Management.Profiles.List(account.Id, sitePropertyList.Id);
            var profileList = profileListRequest.Execute();
            return profileList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site));
        }
    }
}
در اینجا در ابتدا بر اساس فایل p12 ایی که از گوگل دریافت شد، یک X509Certificate2 ایجاد می‌شود. پسورد این فایل مساوی است با ثابت notasecret که در همان زمان تولید اکانت سرویس در گوگل، لحظه‌ای در صفحه نمایش داده خواهد شد. به کمک آن و همچنین ServiceAccountEmail ایمیلی که پیشتر به آن اشاره شد، می‌توان به AnalyticsService لاگین کرد. به این ترتیب به صورت خودکار می‌توان شماره پروفایل اکانت سایت خود را یافت و از آن در حین فراخوانی service.Data.Ga.Get استفاده کرد.


مثالی از نحوه استفاده از کلاس GoogleAnalyticsApiV3

در ادامه یک برنامه‌ی کنسول را ملاحظه می‌کنید که از کلاس GoogleAnalyticsApiV3 استفاده می‌کند:
using System;
using System.Collections.Generic;
using System.Linq;

namespace GoogleAnalyticsAPIv3Tests
{
    class Program
    {

        static void Main(string[] args)
        {
            var statistics = new GoogleAnalyticsApiV3
            {
                Authentication = new AnalyticsAuthentication
                {
                    ApplicationName = "My Project",
                    KeyFilePath = "811e1d9976cd516b55-privatekey.p12",
                    ServiceAccountEmail = "10152bng4j3mq@developer.gserviceaccount.com",
                    SiteUrl = new Uri("https://www.dntips.ir/")
                },
                QueryParameters = new AnalyticsQueryParameters
                {
                    Start = DateTime.Now.AddDays(-7),
                    End = DateTime.Now,
                    Dimensions = "ga:date",
                    Filters = null,
                    Metrics = "ga:users,ga:sessions,ga:pageviews"
                }
            }.GetData();


            foreach (var result in statistics.TotalsForAllResults)
            {
                Console.WriteLine(result.Key + " -> total:" + result.Value);
            }
            Console.WriteLine();

            foreach (var row in statistics.ColumnHeaders)
            {
                Console.Write(row.Name + "\t");
            }
            Console.WriteLine();

            foreach (var row in statistics.Rows)
            {
                var rowItems = (List<string>)row;
                Console.WriteLine(rowItems.Aggregate((s1, s2) => s1 + "\t" + s2));
            }

            Console.ReadLine();
        }
    }
}

چند نکته
ApplicationName همان نام پروژه‌ای است که ابتدای کار، در گوگل ایجاد کردیم.
KeyFilePath مسیر فایل مجوز p12 ایی است که گوگل در حین ایجاد اکانت سرویس، در اختیار ما قرار می‌دهد.
ServiceAccountEmail آدرس ایمیل اکانت سرویس است که در قسمت ادمین Google Analytics به آن دسترسی دادیم.
SiteUrl آدرس سایت شما است که هم اکنون در Google Analytics دارای یک اکانت و پروفایل ثبت شده‌است.
توسط AnalyticsQueryParameters می‌توان نحوه‌ی کوئری گرفتن از Google Analytics را مشخص کرد. تاریخ شروع و پایان گزارش گیری در آن مشخص هستند. در مورد پارامترهایی مانند Dimensions و Metrics بهتر است به مرجع کامل آن در گوگل مراجعه نمائید:
Dimensions & Metrics Reference

برای نمونه در مثال فوق، تعداد کاربران، سشن‌های آن و همچنین تعداد بار مشاهده‌ی صفحات، گزارشگیری می‌شود.


برای مطالعه بیشتر
Using Google APIs in Windows Store Apps
How To Use Google Analytics From C# With OAuth
Google Analytic’s API v3 with C#
.NET Library for Accessing and Querying Google Analytics V3 via Service Account
Google OAuth2 C#
مطالب
استفاده از SQLDom برای آنالیز عبارات T-SQL، قسمت دوم
مدتی قبل مطلبی را در مورد کتابخانه‌ی ویژه SQL Server که یک T-SQL Parser تمام عیار است، در این سایت مطالعه کردید. در این قسمت، همان مطلب را به نحو بهتر و ساده‌تری بازنویسی خواهیم کرد.
مشکلی که در دراز مدت با SQLDom وجود خواهد داشت، مواردی مانند SelectStarExpression و CreateProcedureStatement و امثال آن هستند. این‌ها را از کجا باید تشخیص داد؟ همچنین مراحل بررسی این اجزاء، نسبتا طولانی هستند و نیاز به یک راه حل عمومی‌تر در این زمینه وجود دارد.

راه حلی برای این مشکل در مطلب «XML ‘Visualizer’ for the TransactSql.ScriptDom parse tree» ارائه شده‌است. در اینجا تمام اجزای TSqlFragment توسط Reflection مورد بررسی و استخراج قرار گرفته و نهایتا یک فایل XML از آن حاصل می‌شود.
اگر نکات ذکر شده در این مقاله را تبدیل به یک برنامه با استفاده مجدد کنیم، به چنین شکلی خواهیم رسید:


این برنامه را از اینجا می‌توانید دریافت کنید:
DomToXml.zip

همانطور که در تصویر مشاهده می‌کنید، اینبار به سادگی، SelectStarExpression قابل تشخیص است و تنها کافی است در T-SQL پردازش شده، به دنبال SelectStarExpression‌ها بود. برای اینکار جهت ساده شدن آنالیز می‌توان با ارث بری از کلاس پایه TSqlFragmentVisitor شروع کرد:
using System;
using System.Linq;
using Microsoft.SqlServer.TransactSql.ScriptDom;

namespace DbCop
{
    public class SelectStarExpressionVisitor : TSqlFragmentVisitor
    {
        public override void ExplicitVisit(SelectStarExpression node)
        {
            Console.WriteLine(
                  "`Select *` detected @StartOffset:{0}, Line:{1}, T-SQL: {2}",
                  node.StartOffset,
                  node.StartLine,
                  string.Join(string.Empty, node.ScriptTokenStream.Select(x => x.Text)).Trim());

            base.ExplicitVisit(node);
        }
    }
}
در کلاس پایه TSqlFragmentVisitor به ازای تمام اشیاء شناخته شده‌ی ScriptDom، یک متد ExplicitVisit قابل بازنویسی درنظر گرفته شده‌است. در اینجا برای مثال نمونه‌ی SelectStarExpression آن را بازنویسی کرده‌ایم.
مرحله‌ی بعد، اجرای این کلاس Visitor است:
    public static class GenericVisitor
    {
        public static void Start(string tSql, TSqlFragmentVisitor visitor)
        {
            IList<ParseError> errors;
            TSqlScript sqlFragment;
            using (var reader = new StringReader(tSql))
            {
                var parser = new TSql120Parser(initialQuotedIdentifiers: true);
                sqlFragment = (TSqlScript)parser.Parse(reader, out errors);
            }

            if (errors != null && errors.Any())
            {
                var sb = new StringBuilder();
                foreach (var error in errors)
                    sb.AppendLine(error.Message);

                throw new InvalidOperationException(sb.ToString());
            }
            sqlFragment.Accept(visitor);
        }
    }
در اینجا متد Accept کلاس TSql120Parser، امکان پذیرش یک Visitor را دارد. به این معنا که Parser در حال کار، هر زمانیکه در حال آنالیز قسمتی از T-SQL دریافتی بود، نتیجه را به اطلاع یکی از متدهای کلاس پایه TSqlFragmentVisitor نیز خواهد رساند. بنابراین دیگر نیازی به نوشتن حلقه و بررسی تک تک اجزای خروجی TSql120Parser نیست. اگر نیاز به بررسی SelectStarExpression داریم، فقط کافی است Visitor آن‌را طراحی کنیم.

مثالی از نحوه‌ی استفاده از کلاس GenericVisitor فوق را در اینجا ملاحظه می‌کنید:
 var tsql = @"WITH ctex AS (
SELECT * FROM sys.objects
)
SELECT * FROM ctex";
GenericVisitor.Start(tsql, new SelectStarExpressionVisitor());
مطالب دوره‌ها
تغییر ترتیب آیتم‌های یک لیست به کمک افزونه jquery.sortable در ASP.NET MVC
در این مطلب قصد داریم ترتیب عناصر نمایش داده شده توسط یک لیست را به کمک افزونه بسیار سبک وزن jquery.sortable تغییر داده و نتایج را در سمت سرور مدیریت کنیم. این افزونه بر اساس امکانات کشیدن و رها ساختن HTML5 کار می‌کند و با مرورگرهای IE8 به بعد سازگار است.

مدل‌های برنامه

using System.Collections.Generic;

namespace jQueryMvcSample05.Models
{
    public class Survey
    {
        public int Id { set; get; }
        public string Title { set; get; }

        public virtual ICollection<SurveyItem> SurveyItems { set; get; }
    }
}

namespace jQueryMvcSample05.Models
{
    public class SurveyItem
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public int Order { set; get; }

        //[ForeignKey("SurveyId")]
        public virtual Survey Survey { set; get; }
        public int SurveyId { set; get; }
    }
}
به کمک این ساختار قصد داریم اطلاعات یک سیستم نظر سنجی را نمایش دهیم.
تعدادی نظر سنجی به همراه گزینه‌های آن‌ها تعریف خواهند شد (یک رابطه one-to-many است). سپس توسط افزونه sortable می‌خواهیم ترتیب قرارگیری گزینه‌های آن‌را مشخص کنیم یا تغییر دهیم.


منبع داده فرضی برنامه


using System.Collections.Generic;
using jQueryMvcSample05.Models;

namespace jQueryMvcSample05.DataSource
{
    /// <summary>
    /// یک منبع داده فرضی جهت دموی ساده‌تر برنامه
    /// </summary>
    public static class SurveysDataSource
    {
        private static IList<Survey> _surveysCache;
        static SurveysDataSource()
        {
            _surveysCache = createSurveys();
        }

        public static IList<Survey> SystemSurveys
        {
            get { return _surveysCache; }
        }

        private static IList<Survey> createSurveys()
        {
            var results = new List<Survey>();
            for (int i = 1; i < 6; i++)
            {
                results.Add(new Survey
                {
                    Id = i,
                    Title = "نظر سنجی " + i,
                    SurveyItems = new List<SurveyItem>
                    {
                       new SurveyItem{ Id = 1, SurveyId = i, Title = "گزینه 1", Order = 1 },
                       new SurveyItem{ Id = 2, SurveyId = i, Title = "گزینه 2", Order = 2 },
                       new SurveyItem{ Id = 3, SurveyId = i, Title = "گزینه 3", Order = 3 },
                       new SurveyItem{ Id = 4, SurveyId = i, Title = "گزینه 4", Order = 4 }
                    }
                });
            }
            return results;
        }
    }
}
در اینجا نیز از یک منبع داده فرضی تشکیل شده در حافظه جهت سهولت دموی برنامه استفاده خواهد شد.


کدهای کنترلر برنامه

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample05.DataSource;
using jQueryMvcSample05.Security;

namespace jQueryMvcSample05.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            var surveysList = SurveysDataSource.SystemSurveys;
            return View(surveysList);
        }

        [HttpPost]
        [AjaxOnly]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public ActionResult SortItems(int? surveyId, string[] items)
        {
            if (items == null || items.Length == 0 || surveyId == null)
                return Content("nok");

            updateSurvey(surveyId, items);

            return Content("ok");
        }

        /// <summary>
        /// این متد جهت آشنایی با پروسه به روز رسانی ترتیب گزینه‌ها در اینجا قرار گرفته است
        /// بدیهی است محل قرارگیری آن باید در لایه سرویس برنامه اصلی باشد
        /// </summary>
        private static void updateSurvey(int? surveyId, string[] items)
        {
            var itemIds = new List<int>();
            foreach (var item in items)
            {
                itemIds.Add(int.Parse(item.Replace("item-row-", string.Empty)));
            }

            var survey = SurveysDataSource.SystemSurveys.FirstOrDefault(x => x.Id == surveyId.Value);
            if (survey == null)
                return;

            int order = 0;
            foreach (var itemId in itemIds)
            {
                order++;
                var surveyItem = survey.SurveyItems.FirstOrDefault(x => x.Id == itemId);
                if (surveyItem == null) continue;
                surveyItem.Order = order;
            }

            //todo: call save changes ....
        }
    }
}

و کدهای View متناظر

@model IList<jQueryMvcSample05.Models.Survey>
@{
    ViewBag.Title = "Index";
    var sortUrl = Url.Action(actionName: "SortItems", controllerName: "Home");
}
<h2>
    نظر سنجی‌ها</h2>
@foreach (var survey in Model)
{
    <fieldset>
        <legend>@survey.Title</legend>
        <div id="sortable-@survey.Id">
            @foreach (var surveyItem in survey.SurveyItems.OrderBy(x => x.Order))
            {            
                <div id="item-row-@surveyItem.Id">
                    <span class="handles">::</span>
                    @surveyItem.Title
                </div>
            }
        </div>
    </fieldset>
}
<div>
    لطفا برای تغییر ترتیب آیتم‌های تعریف شده، از امکان کشیدن و رها کردن تعریف شده بر
    روی آیکون‌های :: در کنار هر آیتم استفاده نمائید.
</div>
@section JavaScript
{
    <script type="text/javascript">
        $(document).ready(function () {
            $('div[id^="sortable"]').sortable({ handle: 'span' }).bind('sortupdate', function (e, ui) {
                var sortableItemId = $(ui.item).parent().attr('id');
                var surveyId = sortableItemId.replace('sortable-', '');
                var items = [];
                $('#' + sortableItemId + ' div').each(function () {
                    items.push($(this).attr('id'));
                });
                //alert(items.join('&'));                
                $.ajax({
                    type: "POST",
                    url: "@sortUrl",
                    data: JSON.stringify({ items: items, surveyId: surveyId }),
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    complete: function (xhr, status) {
                        var data = xhr.responseText;
                        if (xhr.status == 403) {
                            window.location = "/login";
                        } else if (status === 'error' || !data || data == "nok") {
                            alert('خطایی رخ داده است');
                        }
                        else {
                            alert('انجام شد');
                        }
                    }
                });
            });
        });
    </script>
}
توضیحات

در اینجا نیاز بود تا ابتدا کدهای کنترلر و View ارائه شوند، تا بتوان در مورد ارتباطات بین آن‌ها بهتر بحث کرد.
در ابتدای نمایش صفحه Home، رکوردهای نظرسنجی‌ها از منبع داده دریافت شده و به View ارسال می‌شوند. در View برنامه یک حلقه تشکیل گردیده و این موارد رندر خواهند شد.
هر نظر سنجی با یک div بیرونی که با id مساوی sortable شروع می‌شود، آغاز گردیده و گزینه‌های آن نظر سنجی نیز توسط divهایی با id مساوی item-row شروع خواهند گردید. هر کدام از این idها حاوی id رکوردهای متناظر هستند. از این id‌ها در کدهای برنامه جهت یافتن یک نظر سنجی یا یک ردیف مشخص برای به روز رسانی ترتیب آن‌ها استفاده خواهیم کرد.
ادامه کار، به تنظیمات و اعمال افزونه sortable مرتبط می‌شود. توسط تنظیم ذیل به jQuery اعلام خواهیم کرد، هرجایی یک div با id شروع شده با sortable یافتی، افزونه sortable را به آن متصل کن:
 $('div[id^="sortable"]').sortable
در ادامه در ناحیه و  div ایی که عمل کشیدن و رها شدن رخ داده، id این div را بدست آورده و سپس کلیه row-itemهای آن را در آرایه‌ای به نام items قرار می‌دهیم:
 var sortableItemId = $(ui.item).parent().attr('id');
var surveyId = sortableItemId.replace('sortable-', '');
var items = [];
$('#' + sortableItemId + ' div').each(function () {
  items.push($(this).attr('id'));
});
اکنون که به id یک نظر سنجی و همچنین idهای ردیف‌های مرتب شده حاصل دسترسی داریم، آن‌ها را توسط jQuery Ajax به کنترلر برنامه ارسال می‌کنیم:
 data: JSON.stringify({ items: items, surveyId: surveyId })
امضای اکشن متد SortItems نیز دقیقا بر همین مبنا تنظیم شده است:
 public ActionResult SortItems(int? surveyId, string[] items)

اطلاعاتی که در اینجا دریافت می‌شوند در متد updateSurvey مورد استفاده قرار خواهند گرفت. بر اساس surveyId دریافتی، نظرسنجی مرتبط را یافته و سپس به گزینه‌های آن دست خواهیم یافت. اکنون نوبت به پردازش آرایه items دریافت شده است. این آرایه بر اساس انتخاب کاربر مرتب شده است.


دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample05.zip
 
نظرات مطالب
RadioButtonList در ASP.NET MVC

من که کدی نمی‌بینم اینجا برای دیباگ و بررسی: آناتومی یک گزارش خطای خوب

ولی احتمالا نام خاصیت ViewModel شما با نام گروهی که تعریف کردید یکی نیست چون در مثال اول بین پارامتر string tech و کلاس زیر فرقی نیست:

public class SomeClass
{
  public string Tech {set; get;}
}
مطالب
سازگار سازی EFTracingProvider با EF Code first
برای ثبت SQL تولیدی توسط EF، ابزارهای پروفایلر زیادی وجود دارند (+). علاوه بر این‌ها یک پروایدر سورس باز نیز برای این منظور به نام EFTracingProvider موجود می‌باشد که برای EF Database first نوشته شده است. در ادامه نحوه‌ی استفاده از این پروایدر را در برنامه‌های EF Code first مرور خواهیم کرد.

الف) دریافت کدهای EFTracingProvider اصلی: (+)
از کدهای دریافتی این مجموعه، فقط به دو پوشه EFTracingProvider و EFProviderWrapperToolkit آن نیاز است.

ب) اصلاح کوچکی در کدهای این پروایدر جهت بررسی نال بودن شیء‌ایی که باید dispose شود
در فایل DbConnectionWrapper.cs، متد Dispose را یافته و به نحو زیر اصلاح کنید (بررسی نال نبودن wrappedConnection اضافه شده است):

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.wrappedConnection != null)
                    this.wrappedConnection.Dispose();
            }

            base.Dispose(disposing);
        }

ج) ساخت یک کلاس پایه Context با قابلیت لاگ فرامین SQL صادره، جهت میسر سازی استفاده مجدد از کدهای آن
د) رفع خطای The given key was not present in the dictionary در حین استفاده از EFTracingProvider

در ادامه کدهای کامل این دو قسمت به همراه یک مثال کاربردی را ملاحظه می‌کنید:

using System;
using System.Configuration;
using System.Data;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations;
using System.Diagnostics;
using System.Linq;
using EFTracingProvider;

namespace Sample
{
    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            var className = this.ContextType.FullName;
            var connectionStringData = ConfigurationManager.ConnectionStrings[className];
            if (connectionStringData == null)
                throw new InvalidOperationException(string.Format("ConnectionStrings[{0}] not found.", className));

            TargetDatabase = new DbConnectionInfo(connectionStringData.ConnectionString, connectionStringData.ProviderName);
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            for (int i = 0; i < 7; i++)
                context.Users.Add(new Person { Name = "name " + i });

            base.Seed(context);
        }
    }

    public class MyContext : MyLoggedContext
    {
        public DbSet<Person> Users { get; set; }
    }

    public abstract class MyLoggedContext : DbContext
    {
        protected MyLoggedContext()
            : base(existingConnection: createConnection(), contextOwnsConnection: true)
        {
            var ctx = ((IObjectContextAdapter)this).ObjectContext;
            ctx.GetTracingConnection().CommandExecuting += (s, e) =>
            {
                Console.WriteLine("{0}\n", e.ToTraceString());
            };
        }

        private static DbConnection createConnection()
        {
            var st = new StackTrace();
            var sf = st.GetFrame(2); // Get the derived class Type in a base class static method
            var className = sf.GetMethod().DeclaringType.FullName;
            
            var connectionStringData = ConfigurationManager.ConnectionStrings[className];
            if (connectionStringData == null)
                throw new InvalidOperationException(string.Format("ConnectionStrings[{0}] not found.", className));

            if (!isEFTracingProviderRegistered())
                EFTracingProviderConfiguration.RegisterProvider();

            EFTracingProviderConfiguration.LogToFile = "log.sql";
            var wrapperConnectionString =
                string.Format(@"wrappedProvider={0};{1}", connectionStringData.ProviderName, connectionStringData.ConnectionString);
            return new EFTracingConnection { ConnectionString = wrapperConnectionString };
        }

        private static bool isEFTracingProviderRegistered()
        {
            var data = (DataSet)ConfigurationManager.GetSection("system.data");
            var providerFactories = data.Tables["DbProviderFactories"];
            return providerFactories.Rows.Cast<DataRow>()
                                         .Select(row => (string)row.ItemArray[1])
                                         .Any(invariantName => invariantName == "EF Tracing Data Provider");
        }
    }

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());
            using (var ctx = new MyContext())
            {
                var users = ctx.Users.AsEnumerable();
                if (users.Any())
                {
                    foreach (var user in users)
                    {
                        Console.WriteLine(user.Name);
                    }
                }

                var rnd = new Random();
                var user1 = ctx.Users.Find(1);
                user1.Name = "test user " + rnd.Next();
                ctx.SaveChanges();
            }

        }
    }
}

توضیحات:
تعریف TargetDatabase در Configuration سبب می‌شود تا خطای The given key was not present in the dictionary در حین استفاده از این پروایدر جدید برطرف شود. به علاوه همانطور که ملاحظه می‌کنید اطلاعات رشته اتصالی بر اساس قراردادهای توکار EF Code first به نام کلاس Context تنظیم شده است.
کلاس MyLoggedContext، کلاس پایه‌ای است که تنظیمات اصلی «EF Tracing Data Provider» در آن قرار گرفته‌اند. برای استفاده از آن باید رشته اتصالی مخصوصی تولید و در اختیار کلاس پایه DbContext قرار گیرد (توسط متد createConnection ذکر شده).
به علاوه در اینجا توسط خاصیت EFTracingProviderConfiguration.LogToFile می‌توان نام فایلی را که قرار است عبارات SQL تولیدی در آن درج شوند، ذکر نمود. همچنین یک روش دیگر دستیابی به کلیه عبارات SQL تولیدی را با مقدار دهی CommandExecuting در سازنده کلاس مشاهده می‌کنید.
اکنون که این کلاس پایه تهیه شده است، تنها کافی است Context معمولی برنامه به نحو زیر تعریف شود:
 public class MyContext : MyLoggedContext
در ادامه اگر متد RunTests را اجرا کنیم، خروجی ذیل را می‌توان در کنسول مشاهده کرد:
insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 0"

insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 1"

insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 2"

insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 3"

insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 4"

insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 5"

insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 6"

SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name]
FROM [dbo].[People] AS [Extent1]

SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name]
FROM [dbo].[People] AS [Extent1]

name 0
name 1
name 2
name 3
name 4
name 5
name 6

update [dbo].[People]
set [Name] = @0
where ([Id] = @1)
-- @0 (dbtype=String, size=-1, direction=Input) = "test user 1355460609"

-- @1 (dbtype=Int32, size=0, direction=Input) = 1

قسمتی از این خروجی مرتبط است به متد Seed تعریف شده که تعدادی رکورد را در بانک اطلاعاتی ثبت می‌کند.
دو select نیز در انتهای کار قابل مشاهده است. اولین مورد به علت فراخوانی متد Any صادر شده است و دیگری به حلقه foreach مرتبط می‌باشد (چون از AsEnumerable استفاده شده، هربار ارجاع به شیء users، یک رفت و برگشت به بانک اطلاعاتی را سبب خواهد شد. برای رفع این حالت می‌توان از متد ToList استفاده کرد.)
در پایان کار، متد update مربوط است به فراخوانی متدهای find و save changes ذکر شده. این خروجی در فایل sql.log نیز در کنار فایل اجرایی برنامه ثبت شده و قابل مشاهده می‌باشد.

کاربردها
اطلاعات این مثال می‌تواند پایه نوشتن یک برنامه entity framework profiler باشد.