مطالب
فارسی نویسی و iTextSharp

شرح یک سری سعی و خطا!
سعی اول:
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var chunk = new Chunk("آزمایش");
pdfDoc.Add(chunk);
}
}
}
}

نتیجه:



بله! هیچی!

مشکل از کجاست؟
در iTextSharp بر اساس نوع فونت انتخابی و encoding مرتبط،‌ نحوه‌ی رندر سازی حروف مشخص می‌شود:



همانطور که ملاحظه می‌کنید، فونت پایه متنی که قرار است اضافه شود، null است.

سعی دوم:
اینبار فونت را تنظیم می‌کنیم:
using System;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
var tahomaFont = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK);

var chunk = new Chunk("آزمایش",tahomaFont);
pdfDoc.Add(chunk);
}
}
}
}

توضیحات:
متد BaseFont.CreateFont می‌تواند مسیری از فونت مورد نظر را دریافت کند. این حالت خصوصا برای برنامه‌های وب که ممکن است فونت مورد نظر آن‌ها در سرور نصب نشده باشد، بسیار مفید است و لزومی ندارد که الزاما فونت مورد استفاده در پوشه fonts‌ ویندوز نصب شده باشد.
نکات مهم دیگر بکار گرفته شده در این متد، استفاده از BaseFont.IDENTITY_H و BaseFont.EMBEDDED است. به این صورت encoding متن، جهت نوشتن متون غیر Ansi تنظیم می‌شود و در این حالت حتما باید فونت را در فایل، مدفون (embed) نمود. از این لحاظ که عموما این نوع فونت‌ها در سیستم‌های کاربران نصب نیستند.

نتیجه:



بد نیست! حداقل حروف نمایش داده شدند؛ اما نیاز است تا چرخانده یا معکوس شوند. برای انجام خودکار آن حداقل دو کار را می‌توان انجام داد.

الف) استفاده از ColumnText و اعمال تنظیمات راست به چپ آن
using System;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
var tahomaFont = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK);

ColumnText ct = new ColumnText(pdfWriter.DirectContent);
ct.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
ct.SetSimpleColumn(100, 100, 500, 800, 24, Element.ALIGN_RIGHT);

var chunk = new Chunk("آزمایش", tahomaFont);

ct.AddElement(chunk);
ct.Go();
}
}
}
}

توضیحات:
در اینجا یک ColumnTex جدید تعریف و سپس خصوصیات این ستون تنظیم شده، به همراه RunDirection آن که اصل قضیه است. سپس chunk تعریف شده را به این ستون اضافه کرده‌ایم.

نتیجه:



بله! کار کرد!

ب) استفاده از PdfTable و اعمال تنظیمات راست به چپ آن
using System;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
var tahomaFont = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK);

PdfPTable table = new PdfPTable(numColumns: 1);
table.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
table.ExtendLastRow = true;

PdfPCell pdfCell = new PdfPCell(new Phrase("آزمایش", tahomaFont));
pdfCell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;

table.AddCell(pdfCell);
pdfDoc.Add(table);
}
}
}
}

در حین استفاده از PdfTable هم لازم است تا RunDirection مربوط به خود جدول و همچنین هر سلول اضافه شده به آن به RTL تنظیم شوند.

این نکات در هر جایی که با این کتابخانه سر و کار داریم باید اعمال شوند. برای مثال:

افزودن Header به صفحات Pdf :
افزودن header در نگارش‌های جدید iTextSharp شامل نکته استفاده از کلاس PdfPageEventHelper به شرح زیر است (و مثال‌هایی را که در وب پیدا خواهید کرد، هیچکدام با آخرین نگارش موجود iTextSharp کار نمی‌کنند):
using System;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
public class PageEvents : PdfPageEventHelper
{
Font _font;
public PageEvents()
{
var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
_font = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK);
}

public override void OnStartPage(PdfWriter writer, Document document)
{
base.OnStartPage(writer, document);

PdfPTable table = new PdfPTable(numColumns: 1);
table.RunDirection = PdfWriter.RUN_DIRECTION_RTL;

PdfPCell pdfCell = new PdfPCell(new Phrase("سر صفحه در صفحه: " + writer.PageNumber, _font));
pdfCell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
pdfCell.HorizontalAlignment = Element.ALIGN_CENTER;

table.AddCell(pdfCell);
document.Add(table);
}
}

class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfWriter.PageEvent = new PageEvents();
pdfDoc.Open();

var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
var tahomaFont = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK);

PdfPTable table = new PdfPTable(numColumns: 1);
table.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
table.ExtendLastRow = true;

PdfPCell pdfCell = new PdfPCell(new Phrase("آزمایش", tahomaFont));
pdfCell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;

table.AddCell(pdfCell);
pdfDoc.Add(table);

pdfDoc.NewPage();
}
}
}
}

نتیجه:



تنها نکته‌ای که اینجا اضافه شده، تعریف کلاس PageEvents است که از کلاس PdfPageEventHelper مشتق شده است. در این کلاس می‌توان یک سری متد کلاس پایه را تحریف کرد و header و footer و غیره را اضافه نمود. سپس جهت اضافه کردن آن، pdfWriter.PageEvent باید مقدار دهی شود.
در اینجا هم اگر نوع فونت، encoding و PdfTable به همراه RunDirection آن اضافه نمی‌شد، یا چیزی در header صفحه قابل مشاهده نبود یا متن مورد نظر معکوس نمایش داده می‌شد.

اشتراک‌ها
تاثیر Index ها در سرعت BulkCopy
به نظر می‌آید برای افزایش سرعت BulkCopy بهتر است قبل از Insert ایندکس‌ها را غیرفعال کرد و بعد از Insert آنها را مجددا فعال کرد.
این کار تاثیر به سزایی در سرعت BulkCopy خصوصا هنگامی که از Clustred Index استفاده شده دارد.
تاثیر Index ها در سرعت BulkCopy
اشتراک‌ها
Visual Studio 2019 version 16.2.2 منتشر شد

Top Issues Fixed in Visual Studio 2019 version 16.2.2

Security Advisory Notices

CVE-2019-1211 Git for Visual Studio Elevation of Privilege Vulnerability

An elevation of privilege vulnerability exists in Git for Visual Studio when it improperly parses configuration files. An attacker who successfully exploited the vulnerability could execute code in the context of another local user. To exploit the vulnerability, an authenticated attacker would need to modify Git configuration files on a system prior to a full installation of the application. The attacker would then need to convince another user on the system to execute specific Git commands. The update addresses the issue by changing the permissions required to edit configuration files. 

Visual Studio 2019 version 16.2.2 منتشر شد
مطالب
SQL Antipattern #1
در این سلسله نوشتار قصد دارم ترجمه و خلاصه چندین فصل از کتاب ارزشمند ( SQL Antipatterns: Avoiding the Pitfalls of Database Programming (Pragmatic Programmers که حاصل تلاش گروه IT موسسه هدایت فرهیختگان جوان می‌باشد، را ارائه نمایم.

بخش اول : Jaywalking  


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

در این صورت، اگر پایگاه داده را به نحوی تغییر دهیم که شناسه‌ی حساب کاربران را در لیستی که با کاما از یکدیگر جدا شده‌اند ذخیره نماییم، خیلی ساده‌تر به نظر می‌رسد نسبت به اینکه بصورت جداگانه آنها را ثبت نماییم.

برنامه نویسان معمولا برای جلوگیری از ایجاد جدول واسطی [1] که رابطه‌های چند به چند زیادی دارد از یک لیست که با کاما داده‌هایش از هم جدا شده‌اند، استفاده می‌کنند. بدین جهت اسم این بخش jaywalking ,antipatten می‌باشد، زیرا jaywalking نیز عملیاتی است که از تقاطع جلوگیری می‌کند.

1.1  هدف:  ذخیره کردن چندین صفت  

طراحی کردن جدولی که ستون آن فقط یک مقدار دارد، بسیار ساده و آسان می‌باشد. شما می‌توانید نوع داده‌ایی که متعلق به ستون می‌باشد را انتخاب نمایید. مثلا از نوع (int,date…)

چگونه می‌توانیم مجموعه‌ایی از مقادیری که به یکدیگر مرتبط هستند را در یک ستون ذخیره نماییم ؟

در مثال ردیابی خطای پایگاه داده‌، ما یک محصول را به یک کاربر، با استفاده از ستونی که نوع آن integer است، مرتبط می‌نماییم. هر کاربر ممکن است محصولاتی داشته باشد و هر محصول به یک contact اشاره کند. بنابراین یک ارتباط چند به یک بین محصولات و کاربر برقرار است. برعکس این موضوع نیز صادق است؛ یعنی امکان دارد هر محصول متعلق به چندین کاربر باشد و یک ارتباط یک به چند ایجاد شود. در زیر جدول محصولات را به صورت عادی آورده شده است: 

CREATE TABLE Products (
product_id SERIAL PRIMARY KEY,
product_name VARCHAR(1000),
account_id BIGINT UNSIGNED,
-- . . .
FOREIGN KEY (account_id) REFERENCES Accounts(account_id)
);

INSERT INTO Products (product_id, product_name, account_id)
VALUES (DEFAULT, 'Visual TurboBuilder' , 12);

2.1 Antipattern : قالب بندی لیست هایی که با کاما از یکدیگر جدا شده اند 
برای اینکه تغییرات در پایگاه داده را به حداقل برسانید، می‌توانید نوع شناسه‌ی کاربر را از نوع varchar قرار دهید. در این صورت می‌توانید چندین شناسه‌ی کاربر که با کاما از یکدیگر جدا شده‌اند را در یک ستون ذخیره نمایید. پس در جدول محصولات، شناسه‌ی کاربران را از نوع varchar قرار می‌دهیم.  
CREATE TABLE Products (
product_id SERIAL PRIMARY KEY,
product_name VARCHAR(1000),
account_id VARCHAR(100), -- comma-separated list
-- . . .
);

INSERT INTO Products (product_id, product_name, account_id)
VALUES (DEFAULT, 'Visual TurboBuilder' , '12,34' );

 اکنون مشکلات کارایی و جامعیت داده‌ها را در این راه حل پیشنهادی بررسی می‌نماییم . 

بدست آوردن محصولاتی برای یک کاربر خاص

بدلیل اینکه تمامی شناسه‌ی کاربران که بصورت کلید خارجی جدول Products می‌باشند به صورت رشته در یک فیلد ذخیره شده‌اند و حالت ایندکس بودن آنها از دست رفته است، بدست آوردن محصولاتی برای یک کاربر خاص سخت می‌باشد. به عنوان مثال بدست آوردن محصولاتی که کاربری با شناسه‌ی 12 خریداری نموده به‌صورت زیر می‌باشد: 

SELECT * FROM Products WHERE account_id REGEXP '[[:<:]]12[[:>:]]' ;
بدست آوردن کاربرانی که یک محصول خاص خریداری نموده اند
 Join کردن account_id با Accounts table  یعنی جدول مرجع نا‌مناسب و غیر معمول می‌باشد. مساله مهمی که وجود دارد این است که زمانیکه بدین صورت شناسه‌ی کاربران را ذخیره می‌نماییم، ایندکس بودن آنها از بین می‌رود. کد زیر کاربرانی که محصولی با شناسه‌ی 123 خریداری کرده‌اند را محاسبه می‌کند.
SELECT * FROM Products AS p JOIN Accounts AS a
ON p.account_id REGEXP '[[:<:]]' || a.account_id || '[[:>:]]'
WHERE p.product_id = 123;

  ایجاد توابع تجمیعی [2]
مبنای چنین توابعی از قبیل sum(), count(), avg() بدین صورت می‌باشد که بر روی گروهی از سطرها اعمال می‌شوند. با این حال با کمک ترفندهایی می‌توان برخی از این توابع را تولید نماییم هر چند برای همه آن‌ها این مساله صادق نمی‌باشد. اگر بخواهیم تعداد کاربران برای هر محصول را بدست بیاوریم ابتدا باید طول لیست شناسه‌ی کاربران را محاسبه کنیم، سپس کاما را از این لیست حذف کرده و طول جدید را از طول قبلی کم کرده و با یک جمع کنیم. نمونه کد آن در زیر آورده شده است:   
SELECT product_id, LENGTH(account_id) - LENGTH(REPLACE(account_id, ',' , '' )) + 1
AS contacts_per_product
FROM Products;

ویرایش کاربرانی که یک محصول خاص خریداری نموده‌اند
ما به راحتی می‌توانیم یک شناسه‌ی جدید را به انتهای این رشته اضافه نماییم؛ فقط مرتب بودن آن را بهم میریزیم. در نمونه اسکریپت زیر گفته شده که در جدول محصولات برای محصولی با شناسه‌ی 123 بعد از کاما یک کاربر با شناسه‌ی 56 درج شود:
UPDATE Products
SET account_id = account_id || ',' || 56
WHERE product_id = 123;
برای حذف یک کاربر از لیست کافیست دو دستور sql را اجرا کرد: اولی برای fetch کردن شناسه‌ی کاربر از لیست و بعدی برای ذخیره کردن لیست ویرایش شده. بطور مثال تمامی افرادی که محصولی با شناسه‌ی 123 را خریداری کرده‌اند، از جدول محصولات انتخاب می‌کنیم. برای حذف کاربری با شناسه‌ی 34 از لیست کاربران، بر حسب کاما مقادیر لیست را در آرایه می‌ریزیم، شناسه‌ی مورد نظر را جستجو می‌کنیم و آن را حذف می‌کنیم. دوباره مقادیر درون آرایه را بصورت رشته درمیاوریم و جدول محصولات را با لیست جدید کاربران برای محصولی با شناسه‌ی 123 بروز‌رسانی می‌کنیم.
<?php
$stmt = $pdo->query(
"SELECT account_id FROM Products WHERE product_id = 123");
$row = $stmt->fetch();
$contact_list = $row['account_id' ];


// change list in PHP code
$value_to_remove = "34";
$contact_list = split(",", $contact_list);
$key_to_remove = array_search($value_to_remove, $contact_list);
unset($contact_list[$key_to_remove]);
$contact_list = join(",", $contact_list);
$stmt = $pdo->prepare(
"UPDATE Products SET account_id = ?
WHERE product_id = 123");
$stmt->execute(array($contact_list));

 اعتبارسنجی شناسه‌ی  محصولات 
به دلیل آنکه نوع فیلد account_id،varchar  می‌باشد احتمال این وجود دارد داده‌ای نامعتبر وارد نماییم چون پایگاه داده‌ها هیچ عکس العملی یا خطایی را نشان نمی‌دهد، فقط از لحاظ معنایی دچار مشکل می‌شویم. در نمونه زیر banana یک داده‌ی غیر معتبر می‌باشد و ارتباطی با شناسه‌ی کاربران ندارد. 
INSERT INTO Products (product_id, product_name, account_id)
VALUES (DEFAULT, 'Visual TurboBuilder' , '12,34,banana' );

انتخاب کردن کاراکتر جداکننده 
نکته قابل توجه این است که کاراکتری که بعنوان جد‌اکننده در نظر می‌گیریم باید در هیچکدام از داده‌های ورودی ما امکان بودنش وجود نداشته باشد .

محدودیت طول لیست 
در زمان طراحی جدول، برای هر یک از فیلد‌ها باید حداکثر طولی را قرار دهیم. به عنوان نمونه ما اگر (varchar(30 در نظر بگیریم بسته به تعداد کاراکتر‌هایی که داده‌های ورودی ما دارند می‌توانیم جدول را پر‌نماییم. اسکریپت زیر شناسه‌ی کاربرانی که محصولی با شناسه‌ی 123 را خریده‌اند، ویرایش می‌کند. با این تفاوت که با توجه به محدودیت لیست، در نمونه‌ی اولی شناسه‌ی کاربران دو کاراکتری بوده و 10داده بعنوان ورودی پذیرفته است در حالیکه در نمونه‌ی دوم بخاطر اینکه شناسه‌ی کاربران شش کاراکتری می‌باشد فقط 4 شناسه می‌توانیم وارد نماییم.
UPDATE Products SET account_id = '10,14,18,22,26,30,34,38,42,46'
WHERE product_id = 123;  
UPDATE Products SET account_id = '101418,222630,343842,467790'
WHERE product_id = 123;

3.1 موارد تشخیص این Antipattern:
سؤالات زیر نشان می‌دهند که Jaywalking antipattern  مورد استفاده قرار گرفته است:
• حداکثر پذیرش این لیست برای داده ورودی چه میزان است؟
• چه کاراکتری هرگز در داده‌های ورودی این لیست ظاهر نمی‌شود؟
4.1  مواردی که استفاده از این Antipattern مجاز است:
دی نرمال کردن کارایی را افزایش می‌دهد. ذخیره کردن شناسه‌ها در یک لیست که با کاما از یکدیگر جدا شده‌اند نمونه بارزی از دی نرمال کردن می‌باشد. ما زمانی می‌توانیم از این مدل استفاده نماییم که به جدا کردن شناسه‌ها از هم نیاز نداشته باشیم. به همین ترتیب، اگر در برنامه، شما یک لیست را از یک منبع دیگر با این فرمت دریافت نمایید، به سادگی آن را در پایگاه داده خود به همان فرمت بصورت کامل می‌توانید ذخیره و بازیابی نمایید و احتیاجی به مقادیر جداگانه ندارید. درهنگام استفاده از پایگاه داده‌های دی‌نرمال دقت کنید. برای شروع از پایگاه داده نرمال استفاده کنید زیرا به کدهای برنامه شما امکان انعطاف بیشتری می‌دهد و کمک می‌کند تا پایگاه داده‌ها تمامیت داده‌(Data integrity) را حفظ کند.
5.1  راه حل: استفاده کردن از جدول واسط
در این روش شناسه‌ی کاربران را در جدول جداگانهایی که هر کدام از آنها یک سطر را به خود اختصاص داده اند، ذخیره می‌نماییم.
CREATE TABLE Contacts ( product_id BIGINT UNSIGNED NOT NULL,
account_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (product_id, account_id),
FOREIGN KEY (product_id) REFERENCES Products(product_id),
FOREIGN KEY (account_id) REFERENCES Accounts(account_id)
);

جدول Contacts یک جدول رابطه ایی بین جداول Products,Accounts

جدول Contacts یک جدول رابطه ایی بین جداول Products,Accounts 


 بدست آوردن محصولات برای کاربران و موارد مربوط به آن 

ما براحتی می‌توانیم تمامی محصولاتی که مختص به یک کاربر هستند را بدست آوریم. در این شیوه خاصیت ایندکس بودن شناسه‌ی کاربران حفظ می‌شود به همین دلیل queryهای آن برای خواندن و بهینه کردن راحت‌تر می‌باشند. در این روش به کاراکتری برای جدا کردن ورودی‌ها از یکدیگر نیاز نداریم چون هر کدام از آنها در یک سطر جداگانه ثبت می‌شوند. برای ویرایش کردن کاربرانی که یک محصول را خریداری نموده اند، کافیست یک سطر از جدول واسط را اضافه یا حذف نماییم. درنمونه کد زیر، ابتدا در جدول Contacts کاربری با شناسه‌ی 34 که محصولی با شناسه‌ی 456 را خریداری کرده، درج شده است و در خط بعد عملیات حذف با شرط آنکه شناسه‌ی کاربر و محصول به ترتیب 34،456 باشد روی جدول Contacts اعمال شده است.

INSERT INTO Contacts (product_id, account_id) VALUES (456, 34);

DELETE FROM Contacts WHERE product_id = 456 AND account_id = 34;

ایجاد توابع تجمیعی

به عنوان نمونه در مثال زیر براحتی ما می‌توانیم تعداد محصولات در هر حساب کاربری را بدست آوریم:

SELECT account_id, COUNT(*) AS products_per_account
FROM Contacts
GROUP BY account_id;

اعتبارسنجی شناسه  محصولات 

از آنجاییکه مقادیری که در جدول قرار دارند کلید خارجی می‌باشند میتوان صحت اعتبار آنها را بررسی نمود. بعنوان مثال Contacts.account_id به Account.account_id  اشاره می‌کند. در ضمن برای هر فیلد نوع آن را می‌توان مشخص کرد تا فقط همان نوع داده را بپذیرد.

محدودیت طول لیست 

نسبت به روش قبلی تقریبا در این حالت محدودیتی برای تعداد کاراکتر‌های ورودی نداریم.


مزیت‌های دیگر استفاده از جدول واسط

کارایی روش دوم بهتر از حالت قبلی می‌باشد چون ایندکس بودن شناسه‌ها حفظ شده است. همچنین براحتی میتوانیم فیلدی را به این جدول اضافه نماییم مثلا (time, date… ) 

  


[1] Intersection Table  
[2] Aggregate  
مطالب
lambda expression در Vb.net
با vb.net تو پروژه vs2012 خیلی بیرحمانه رفتار شد. خیلی از برنامه نویسان هنوز فکر میکنن vb.net زبان آموزشه نه برنامه نویسی. حالا با اومدن MVC, EF4, ParallelPrograming, Async و غیره موضوع بدتر شده و  در اکثر وبسایتها معلولا بحث و sampleها با زبان #C ارائه میشه. اکثر Pattern که نوشته میشه به زبان #C. تو MVC4 بوضوع میتونید اینو لمس کنید.
امروز میخوام چندتا مثال از دستورات EF با  lambda expression بنویسم.
دستور Select
Dim query = db.table.Select(Function(q)  q)
Select سفارشی
Dim query = db.table.Select(Function(q) New With{q.field1,q.field2,...})
Select  پردن کردن فیلدهای یک Property
class:
Public Class person
Public Property id As Integet
Public Property fname As String
End Class
entity commmand:
Dim query = db.table.Where(Function(q) New person With{.id=q.idfield,.fname=q.namefield})
دستور where، single
Dim query = db.table.Where(Function(q) q.yourfield = yourvaribale).Select(Function(p) p).Single()
دستور join و group
join:
Dim query = db.table1.Join(db.table2, Function(q) q.youridfield, Function(p) p.youridfield, Function(q, p) New With {Key.q= q, Key.p= p})
group:
Dim query = db.table.GroupBy(Function(q) q.groupkey).Select(Function(p) p.Sum(Function(w) w.aggregate)) 
مابقی دستورات مسه iinsert,delete,update نگذاشتم. اگر دوستان مایل بودن pm بدن این دستوراتم براشون میزارم
موفق باشید
مطالب دوره‌ها
استفاده از AOP Interceptors برای حذف کدهای تکراری INotifyPropertyChanged در WPF
هرکسی که با WPF کار کرده باشد با دردی به نام اینترفیس INotifyPropertyChanged و پیاده سازی‌های تکراری مرتبط با آن آشنا است:
public class MyClass : INotifyPropertyChanged
{
    private string _myValue;
    public event PropertyChangedEventHandler PropertyChanged;
    public string MyValue
    {
        get
        {
            return _myValue;
        }
        set
        {
            _myValue = value;
            RaisePropertyChanged("MyValue");
        }
    }
    protected void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}
چندین راه‌حل هم برای ساده سازی و یا بهبود آن وجود دارد از Strongly typed کردن آن تا روش‌های اخیر دات نت 4 و نیم در مورد استفاده از ویژگی‌های متدهای فراخوان. اما ... با استفاده از AOP Interceptors می‌توان در وهله سازی‌ها و فراخوانی‌ها دخالت کرد و کدهای مورد نظر را در مکان‌های مناسبی تزریق نمود. بنابراین در مطلب جاری قصد داریم ارائه متفاوتی را از پیاده سازی خودکار INotifyPropertyChanged ارائه دهیم. به عبارتی چقدر خوب می‌شد فقط می‌نوشتیم :
public class MyDreamClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public string MyValue { get; set; }
}
و ... همه چیز مثل سابق کار می‌کرد. برای رسیدن به این هدف، باید فراخوانی‌های set خواص را تحت نظر قرار داد (یا همان Interception در اینجا). ابتدا باید اجازه دهیم تا set صورت گیرد، پس از آن کدهای معروف RaisePropertyChanged را به صورت خودکار فراخوانی کنیم.


پیشنیازها

ابتدا یک برنامه جدید WPF را آغاز کنید. تنظیمات آن‌را از حالت Client profile به Full تغییر دهید.
سپس همانند قسمت قبل، ارجاعات لازم را به StructureMap و Castle.Core نیز اضافه نمائید:
 PM> Install-Package structuremap
PM> Install-Package Castle.Core


ساختار برنامه

برنامه ما از یک اینترفیس و کلاس سرویس تشکیل شده است:
namespace AOP01.Services
{
    public interface ITestService
    {
        int GetCount();
    }
}

namespace AOP01.Services
{
    public class TestService: ITestService
    {     
        public int GetCount()
        {
            return 10; //این فقط یک مثال است برای بررسی تزریق وابستگی‌ها
        }
    }
}
همچنین دارای یک ViewModel به شکل زیر می‌باشد:
using AOP01.Services;
using AOP01.Core;

namespace AOP01.ViewModels
{
    public class TestViewModel  : BaseViewModel
    {
        private readonly ITestService _testService;
        //تزریق وابستگی‌ها در سازنده کلاس
        public TestViewModel(ITestService testService)
        {
            _testService = testService;
        }

        // Note: it's a virtual property.
        public virtual string Text { get; set; }
    }
}
سه نکته در این ViewModel حائز اهمیت هستند:
الف) استفاده از کلاس پایه BaseViewModel برای کاهش کدهای تکراری مرتبط با INotifyPropertyChanged که به صورت زیر تعریف شده است:
using System.ComponentModel;

namespace AOP01.Core
{
    public abstract class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void RaisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;

            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
ب) کلاس سرویس، در حالت تزریق وابستگی‌ها در سازنده کلاس در اینجا مورد استفاده قرار گرفته است. وهله سازی خودکار آن توسط کلاس‌های پروکسی و DI صورت خواهند گرفت.
ج) خاصیتی که در اینجا تعریف شده از نوع virtual است؛ بدون پیاده سازی مفصل قسمت set آن و فراخوانی مستقیم RaisePropertyChanged کلاس پایه به صورت متداول. علت virtual تعریف کردن آن به امکان دخل و تصرف در نواحی get و set این خاصیت توسط Interceptor ایی که در ادامه تعریف خواهیم کرد بر می‌گردد.


پیاده سازی NotifyPropertyInterceptor

using System;
using Castle.DynamicProxy;

namespace AOP01.Core
{
    public class NotifyPropertyInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            // متد ست، ابتدا فراخوانی می‌شود و سپس کار اطلاع رسانی را انجام خواهیم داد
            invocation.Proceed();

            if (invocation.Method.Name.StartsWith("set_"))
            {
                var propertyName = invocation.Method.Name.Substring(4);
                raisePropertyChangedEvent(invocation, propertyName, invocation.TargetType);
            }
        }

        void raisePropertyChangedEvent(IInvocation invocation, string propertyName, Type type)
        {
            var methodInfo = type.GetMethod("RaisePropertyChanged");
            if (methodInfo == null)
            {
                if (type.BaseType != null)
                    raisePropertyChangedEvent(invocation, propertyName, type.BaseType);
            }
            else
            {
                methodInfo.Invoke(invocation.InvocationTarget, new object[] { propertyName });
            }
        }
    }
}
با اینترفیس IInterceptor در قسمت قبل آشنا شدیم.
در اینجا ابتدا اجازه خواهیم داد تا کار set به صورت معمول انجام شود. دو حالت get و set ممکن است رخ دهند. بنابراین در ادامه بررسی خواهیم کرد که اگر حالت set بود، آنگاه متد RaisePropertyChanged کلاس پایه BaseViewModel را یافته و به صورت پویا با propertyName صحیحی فراخوانی می‌کنیم.
به این ترتیب دیگر نیازی نخواهد بود تا به ازای تمام خواص مورد نیاز، کار فراخوانی دستی RaisePropertyChanged صورت گیرد.


اتصال Interceptor به سیستم

خوب! تا اینجای کار صرفا تعاریف اولیه تدارک دیده شده‌اند. در ادامه نیاز است تا DI و DynamicProxy را از وجود آن‌ها مطلع کنیم.
برای این منظور فایل App.xaml.cs را گشوده و در نقطه آغاز برنامه تنظیمات ذیل را اعمال نمائید:
using System.Linq;
using System.Windows;
using AOP01.Core;
using AOP01.Services;
using Castle.DynamicProxy;
using StructureMap;

namespace AOP01
{
    public partial class App
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            ObjectFactory.Initialize(x =>
            {
                x.For<ITestService>().Use<TestService>();

                var dynamicProxy = new ProxyGenerator();
                x.For<BaseViewModel>().EnrichAllWith(vm =>
                {
                    var constructorArgs = vm.GetType()
                            .GetConstructors()
                            .FirstOrDefault()
                            .GetParameters()
                            .Select(p => ObjectFactory.GetInstance(p.ParameterType))
                            .ToArray();

                    return dynamicProxy.CreateClassProxy(
                                classToProxy: vm.GetType(),
                                constructorArguments: constructorArgs,
                                interceptors: new[] { new NotifyPropertyInterceptor() });
                });
            });
        }
    }
}
مطابق این تنظیمات، هرجایی که نیاز به نوعی از ITestService بود، از کلاس TestService استفاده خواهد شد.
همچنین در ادامه به DI مورد استفاده اعلام می‌کنیم که ViewModelهای ما دارای کلاس پایه BaseViewModel هستند. بنابراین هر زمانی که این نوع موارد وهله سازی شدند، آن‌ها را یافته و با پروکسی حاوی NotifyPropertyInterceptor مزین کن.
مثالی که در اینجا انتخاب شده، تقریبا مشکل‌ترین حالت ممکن است؛ چون به همراه تزریق خودکار وابستگی‌ها در سازنده کلاس ViewModel نیز می‌باشد. اگر ViewModelهای شما سازنده‌ای به این شکل ندارند، قسمت تشکیل constructorArgs را حذف کنید.


استفاده از ViewModel مزین شده با پروکسی در یک View

<Window x:Class="AOP01.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TextBox Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    </Grid>
</Window>
اگر فرض کنیم که پنجره اصلی برنامه مصرف کننده ViewModel فوق است، در code behind آن خواهیم داشت:
using AOP01.ViewModels;
using StructureMap;

namespace AOP01
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            //علاوه بر تشکیل پروکسی
            //کار وهله سازی و تزریق وابستگی‌ها در سازنده را هم به صورت خودکار انجام می‌دهد
            var vm = ObjectFactory.GetInstance<TestViewModel>(); 
            this.DataContext = vm;
        }
    }
}
به این ترتیب یک ViewModel محصور شده توسط DynamicProxy مزین با NotifyPropertyInterceptor به DataContext  ارسال می‌گردد.

اکنون اگر برنامه را اجرا کنیم، مشاهده خواهیم کرد که با وارد کردن مقداری در TextBox برنامه، NotifyPropertyInterceptor مورد استفاده قرار می‌گیرد:



دریافت مثال کامل این قسمت
AOP01.zip
نظرات مطالب
EF Code First #14
درسته جناب نصیری ولی در صورتی که پروژه به صورت SOA باشه  مثل WCF (خواه Win App باشد خواه Web App) دیگه Context در سطح فرم معنی پیدا نمی‌کنه . در این حالت اصلا Context سمت کلاینت وجود ندارد که بتونیم ردیابی خودکار اشیا را به عهده اون بزاریم. سمت کلاینت فقط مدل برنامه است به علاوه یک ChannelFactory برای ارتباط با سرور .در این حالت  خود مدل‌های برنامه باید توانایی Track کردن رو داشته باشند و Context سمت برنامه با استفاده از این اطلاعات Track شده توسط Entity عملیات CRUD را رو دیتابیس اجرا می‌کنه.
در این حالت  می‌تونیم N  تا Entity رو که هر کدوم یک State مشخص دارند مثل Addedd , Deleted , modified توسط متد ApplyChanged که برای ObjectContext تعریف شده به ObjectStateManager اضافه  کنیم و در نهایت با دستور SaveChanged اطلاعات به صورت نهایی روی دیتابیس اعمال می‌شوند.
توضیحات تکمیلی (^ )
مطالب
کار با چندین نوع بانک اطلاعاتی متفاوت در Entity Framework Core
یکی از مزایای کار با ORMها، امکان تعویض نوع بانک اطلاعاتی برنامه، بدون نیازی به تغییری در کدهای برنامه است. برای مثال فرض کنید می‌خواهید با تغییر رشته‌ی اتصالی برنامه، یکبار از بانک اطلاعاتی SQL Server و بار دیگر از بانک اطلاعاتی کاملا متفاوتی مانند SQLite استفاده کنید. در این مطلب نکات استفاده‌ی از چندین نوع بانک اطلاعاتی متفاوت را در برنامه‌های مبتنی بر EF Core بررسی خواهیم کرد.


هر بانک اطلاعاتی باید Migration و Context خاص خودش را داشته باشد

تامین کننده‌ی بانک‌های اطلاعاتی مختلف، عموما تنظیمات خاص خودشان را داشته و همچنین دستورات SQL متفاوتی را نیز تولید می‌کنند. به همین جهت نمی‌توان از یک تک Context، هم برای SQLite و هم SQL Server استفاده کرد. به علاوه قصد داریم اطلاعات Migrations هر کدام را نیز در یک اسمبلی جداگانه قرار دهیم. در یک چنین حالتی EF نمی‌پذیرد که Context تولید کننده‌ی Migration، در اسمبلی دیگری قرار داشته باشد و باید حتما در همان اسمبلی Migration قرار گیرد. بنابراین ساختار پوشه بندی مثال جاری به صورت زیر خواهد بود:


- در پوشه‌ی EFCoreMultipleDb.DataLayer فقط اینترفیس IUnitOfWork را قرار می‌دهیم. از این جهت که وقتی قرار شد در برنامه چندین Context تعریف شوند، لایه‌ی سرویس برنامه قرار نیست بداند در حال حاضر با کدام Context کار می‌کند. به همین جهت است که تغییر بانک اطلاعاتی برنامه، تغییری را در کدهای اصلی آن ایجاد نخواهد کرد.
- در پوشه‌ی EFCoreMultipleDb.DataLayer.SQLite کدهای Context و همچنین IDesignTimeDbContextFactory مخصوص SQLite را قرار می‌دهیم.
- در پوشه‌ی EFCoreMultipleDb.DataLayer.SQLServer کدهای Context و همچنین IDesignTimeDbContextFactory مخصوص SQL Server را قرار می‌دهیم.

برای نمونه ابتدای Context مخصوص SQLite چنین شکلی را دارد:
    public class SQLiteDbContext : DbContext, IUnitOfWork
    {
        public SQLiteDbContext(DbContextOptions options) : base(options)
        { }

        public virtual DbSet<User> Users { set; get; }
و IDesignTimeDbContextFactory مخصوص آن که برای Migrations از آن استفاده می‌شود، به صورت زیر تهیه خواهد شد:
namespace EFCoreMultipleDb.DataLayer.SQLite.Context
{
    public class SQLiteDbContextFactory : IDesignTimeDbContextFactory<SQLiteDbContext>
    {
        public SQLiteDbContext CreateDbContext(string[] args)
        {
            var basePath = Directory.GetCurrentDirectory();
            Console.WriteLine($"Using `{basePath}` as the BasePath");
            var configuration = new ConfigurationBuilder()
                                    .SetBasePath(basePath)
                                    .AddJsonFile("appsettings.json")
                                    .Build();
            var builder = new DbContextOptionsBuilder<SQLiteDbContext>();
            var connectionString = configuration.GetConnectionString("SqliteConnection")
                                                .Replace("|DataDirectory|", Path.Combine(basePath, "wwwroot", "app_data"));
            builder.UseSqlite(connectionString);
            return new SQLiteDbContext(builder.Options);
        }
    }
}
هدف از این فایل، ساده سازی کار تولید اطلاعات Migrations برای EF Core است. به این صورت ساخت new SQLiteDbContext توسط ما صورت خواهد گرفت و دیگر EF Core درگیر جزئیات وهله سازی آن نمی‌شود.


تنظیمات رشته‌های اتصالی بانک‌های اطلاعاتی مختلف

در اینجا محتویات فایل appsettings.json را که در آن تنظیمات رشته‌های اتصالی دو بانک SQL Server LocalDB و همچنین SQLite در آن ذکر شده‌اند، مشاهده می‌کنید:
{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SqlServerConnection": "Data Source=(LocalDB)\\MSSQLLocalDB;Initial Catalog=ASPNETCoreSqlDB;AttachDbFilename=|DataDirectory|\\ASPNETCoreSqlDB.mdf;Integrated Security=True;MultipleActiveResultSets=True;",
    "SqliteConnection": "Data Source=|DataDirectory|\\ASPNETCoreSqliteDB.sqlite",
    "InUseKey": "SqliteConnection"
  }
}
همین رشته‌ی اتصالی است که در SQLiteDbContextFactory مورد استفاده قرار می‌گیرد.
یک کلید InUseKey را هم در اینجا تعریف کرده‌ایم تا مشخص باشد در ابتدای کار برنامه، کلید کدام رشته‌ی اتصالی مورد استفاده قرار گیرد. برای مثال در اینجا کلید رشته‌ی اتصالی SQLite تنظیم شده‌است.
در این تنظیمات یک DataDirectory را نیز مشاهده می‌کنید. مقدار آن در فایل Startup.cs برنامه به صورت زیر بر اساس پوشه‌ی جاری تعیین می‌شود و در نهایت به wwwroot\app_data اشاره خواهد کرد:
var connectionStringKey = Configuration.GetConnectionString("InUseKey");
var connectionString = Configuration.GetConnectionString(connectionStringKey)
                     .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data"));


دستورات تولید Migrations و به روز رسانی بانک اطلاعاتی

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

برای تولید Migrations، از طریق خط فرمان، به پوشه‌ی اسمبلی مدنظر وارد شده و دستور زیر را اجرا کنید:
For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c_%%a_%%b)
For /f "tokens=1-2 delims=/:" %%a in ("%TIME: =0%") do (set mytime=%%a%%b)
dotnet build
dotnet ef migrations --startup-project ../EFCoreMultipleDb.Web/ add V%mydate%_%mytime% --context SQLiteDbContext
در اینجا ذکر startup-project و همچنین context برای پروژه‌هایی که context آن‌ها خارج از startup-project است و همچنین بیش از یک context دارند، ضروری‌است. بدیهی است این دستورات را باید یکبار در پوشه‌ی EFCoreMultipleDb.DataLayer.SQLite و یکبار در پوشه‌ی EFCoreMultipleDb.DataLayer.SQLServer اجرا کنید.
دو سطر اول آن، زمان اجرای دستورات را به عنوان نام فایل‌ها تولید می‌کنند.

پس از تولید Migrations، اکنون نوبت به تولید بانک اطلاعاتی و یا به روز رسانی بانک اطلاعاتی موجود است:
dotnet build
dotnet ef --startup-project ../EFCoreMultipleDb.Web/ database update --context SQLServerDbContext
در این مورد نیز ذکر startup-project و همچنین context مدنظر ضروری است.


بدیهی است این رویه را پس از هربار تغییراتی در موجودیت‌های برنامه و یا تنظیمات آن‌ها در Contextهای متناظر، نیاز است مجددا اجرا کنید. البته اجرای اولین دستور اجباری است؛ اما می‌توان دومین دستور را به صورت زیر نیز اجرا کرد:
namespace EFCoreMultipleDb.Web
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            applyPendingMigrations(app);
// ...
        }

        private static void applyPendingMigrations(IApplicationBuilder app)
        {
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var uow = scope.ServiceProvider.GetService<IUnitOfWork>();
                uow.Migrate();
            }
        }
    }
}
متد applyPendingMigrations، کار وهله سازی IUnitOfWork را انجام می‌دهد. سپس متد Migrate آن‌را اجرا می‌کند، تا تمام Migrations تولید شده، اما اعمال نشده‌ی به بانک اطلاعاتی، به صورت خودکار به آن اعمال شوند. متد Migrate نیز به صورت زیر تعریف می‌شود:
namespace EFCoreMultipleDb.DataLayer.SQLite.Context
{
    public class SQLiteDbContext : DbContext, IUnitOfWork
    {
    // ... 

        public void Migrate()
        {
            this.Database.Migrate();
        }
    }
}

مرحله‌ی آخر: انتخاب بانک اطلاعاتی در برنامه‌ی آغازین

پس از این تنظیمات، قسمتی که کار تعریف IUnitOfWork و همچنین DbContext جاری برنامه را انجام می‌دهد، به صورت زیر پیاده سازی می‌شود:
namespace EFCoreMultipleDb.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUsersService, UsersService>();

            var connectionStringKey = Configuration.GetConnectionString("InUseKey");
            var connectionString = Configuration.GetConnectionString(connectionStringKey)
                     .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data"));
            switch (connectionStringKey)
            {
                case "SqlServerConnection":
                    services.AddScoped<IUnitOfWork, SQLServerDbContext>();
                    services.AddDbContext<SQLServerDbContext>(options =>
                    {
                        options.UseSqlServer(
                            connectionString,
                            dbOptions =>
                                {
                                    var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                                    dbOptions.CommandTimeout(minutes);
                                    dbOptions.EnableRetryOnFailure();
                                });
                    });
                    break;
                case "SqliteConnection":
                    services.AddScoped<IUnitOfWork, SQLiteDbContext>();
                    services.AddDbContext<SQLiteDbContext>(options =>
                    {
                        options.UseSqlite(
                            connectionString,
                            dbOptions =>
                                {
                                    var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                                    dbOptions.CommandTimeout(minutes);
                                });
                    });
                    break;
                default:
                    throw new NotImplementedException($"`{connectionStringKey}` is not defined in `appsettings.json` file.");
            }

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }
در اینجا ابتدا مقدار InUseKey از فایل تنظیمات برنامه دریافت می‌شود. بر اساس مقدار آن، رشته‌ی اتصالی مدنظر دریافت شده و سپس یکی از دو حالت SQLite و یا SQLServer انتخاب می‌شوند. برای مثال اگر Sqlite انتخاب شده باشد، IUnitOfWork به SQLiteDbContext تنظیم می‌شود. به این ترتیب لایه‌ی سرویس برنامه که با IUnitOfWork کار می‌کند، به صورت خودکار وهله‌ای از SQLiteDbContext را دریافت خواهد کرد.


آزمایش برنامه

ابتدا کدهای کامل این مطلب را از اینجا دریافت کنید: EFCoreMultipleDb.zip
سپس آن‌را اجرا نمائید. چنین تصویری را مشاهده خواهید کرد:


اکنون برنامه را بسته و سپس فایل appsettings.json را جهت تغییر مقدار InUseKey به کلید SqlServerConnection ویرایش کنید:
{
  "ConnectionStrings": {
    // …
    "InUseKey": "SqlServerConnection"
  }
}
اینبار اگر مجددا برنامه را اجرا کنید، چنین خروجی قابل مشاهده‌است:


مقدار username، در contextهای هر کدام از این بانک‌های اطلاعاتی، با مقدار متفاوتی به عنوان اطلاعات اولیه‌ی آن ثبت شده‌است. سرویسی هم که اطلاعات آن‌را تامین می‌کند، به صورت زیر تعریف شده‌است:
namespace EFCoreMultipleDb.Services
{
    public interface IUsersService
    {
        Task<User> FindUserAsync(int userId);
    }

    public class UsersService : IUsersService
    {
        private readonly IUnitOfWork _uow;
        private readonly DbSet<User> _users;

        public UsersService(IUnitOfWork uow)
        {
            _uow = uow;
            _users = _uow.Set<User>();
        }

        public Task<User> FindUserAsync(int userId)
        {
            return _users.FindAsync(userId);
        }
    }
}
همانطور که مشاهده می‌کنید، با تغییر context برنامه، هیچ نیازی به تغییر کدهای UsersService نیست؛ چون اساسا این سرویس نمی‌داند که IUnitOfWork چگونه تامین می‌شود.
اشتراک‌ها
NET Core 3 Preview 3. منتشر شد

Today, we are announcing .NET Core 3.0 Preview 3. We would like to update you on the .NET Core 3.0 schedule and introduce you to improvements in .NET Core SDK installers, Docker containers, Range, and Index. We also have updates on the Windows Desktop and Entity Framework projects. 

NET Core 3 Preview 3. منتشر شد
اشتراک‌ها
ASP.NET Core و سازمان‌ها

However, it is clear that Microsoft’s future direction is in the Core space . Yes, the last update to the full .NET framework did include improvements for ASP.NET and WebForms, but clearly the future innovation and hard work will be in the new core frameworks like .NET core, ASP.NET Core, Entity Framework core, and whatever other cores come along in the future. 

ASP.NET Core و سازمان‌ها