Web Payments is an emerging web standard being developed by the W3C to simplify online payments and enable a broader set of players to participate easily in the payments ecosystem on the web. The standards are flexible; they work with various types of payment systems and are intended to work on any browser on any device, payment method, or payment service provider. This flexibility enables development simplicity, deployment consistency, and future compatibility with emerging payment technologies.
TypeScript 5.2 منتشر شد
Here’s a quick list of what’s new in TypeScript 5.2!
using Declarations and Explicit Resource Management
Decorator Metadata
Named and Anonymous Tuple Elements
Easier Method Usage for Unions of Arrays
Copying Array Methods
symbols as WeakMap and WeakSet Keys
Type-Only Import Paths with TypeScript Implementation File Extensions
Comma Completions for Object Members
Inline Variable Refactoring
Clickable Inlay Parameter Hints
Optimized Checks for Ongoing Type Compatibility
Breaking Changes and Correctness Fixes
The toolkit contains various controls (labesl, text entry, buttons, radio buttons, checkboxes, dialog boxes, windows, menus) for building text user interfaces, a main loop, is designed to work on Curses and the Windows Console, works well on both color and monochrome terminals and has mouse support on terminal emulators that support it.
استفاده از Log4Net جهت ثبت خروجیهای SQL حاصل از NHibernate
هنگام استفاده از NHibernate، پس از افزودن ارجاعات لازم به اسمبلیهای مورد نیاز آن به برنامه، یکی از اسمبلیهایی که به پوشه build برنامه به صورت خودکار کپی میشود، فایل log4net.dll است (حتی اگر ارجاعی را به آن اضافه نکرده باشیم) که جهت ثبت وقایع مرتبط با NHibernate مورد استفاده قرار میگیرد. خوب اگر مجبوریم که این وابستگی کتابخانه NHibernate را نیز در پروژههای خود داشته باشیم، چرا از آن استفاده نکنیم؟!
شرح مفصل استفاده از این کتابخانه سورس باز را در سایت اصلی آن میتوان مشاهده کرد:
برای اینکه از این کتابخانه در برنامه خود جهت ثبت عبارات SQL تولیدی توسط NHibernate استفاده کنیم، باید مراحل زیر طی شوند:
الف) ارجاعی را به اسمبلی log4net.dll اضافه نمائید (کنار اسمبلی NHibernate در پوشه build برنامه باید موجود باشد)
ب) فایل app.config برنامه را (برنامه ویندوزی) به صورت زیر ویرایش کرده و چند سطر مربوطه را اضافه نمائید (در مورد برنامههای وب هم به همین شکل است. configSections فایل web.config تنظیم شده و سپس تنظیمات log4net را قبل از بسته شدن تگ configuration اضافه نمائید ) :
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net"
type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
</configSections>
<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true"/>
</connectionStrings>
<log4net>
<appender name="rollingFile"
type="log4net.Appender.RollingFileAppender,log4net" >
<param name="File" value="NHibernate_Log.txt" />
<param name="AppendToFile" value="true" />
<param name="DatePattern" value="yyyy.MM.dd" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="10" />
<maximumFileSize value="500KB" />
<staticLogFileName value="true" />
<layout type="log4net.Layout.PatternLayout,log4net">
<conversionPattern value="%d %p %m%n" />
</layout>
</appender>
<logger name="NHibernate.SQL">
<level value="ALL" />
<appender-ref ref="rollingFile" />
</logger>
</log4net>
</configuration>
log4net.Config.XmlConfigurator.Configure();
یا در یک برنامه از نوع WinForms تنها کافی است سطر زیر را به فایل AssemblyInfo.cs برنامه اضافه کرد:
// Configure log4net using the .config file
[assembly: log4net.Config.XmlConfigurator(Watch = true)]
د) هنگام استفاده از کتابخانه Fluent NHibernate حتما باید متد ShowSql در جایی که دیتابیس برنامه را تنظیم میکنیم (Fluently.Configure().Database) ذکر گردد (که نمونه آنرا در مثالهای قسمتهای قبل ملاحظه کردهاید).
توضیحاتی در مورد تنظیمات فوق:
configSections حتما باید در ابتدای فایل app.config ذکر شود و گرنه برنامه کار نخواهد کرد.
سپس کانکشن استرینگ مورد استفاده در قسمت کانفیگ برنامه ذکر شده است.
در ادامه تنظیمات استاندارد مربوط به log4net را مشاهده میکنید.
در تنظیمات این کتابخانه، appender مشخص کننده محل ثبت وقایع است. زمانیکه که از RollingFileAppender استفاده کنیم، اطلاعات را در یک سری فایل ذخیره خواهد کرد (امکان ثبت وقایع در EventLog ویندوز، ارسال از طریق ایمیل و غیره نیز میسر است که جهت توضیحات بیشتر میتوان به مستندات آن رجوع نمود).
سپس نام فایلی که اطلاعات وقایع در آن ثبت خواهند شد ذکر شده است (برای مثال NHibernate_Log.txt)، در ادامه مشخص گردیده که اطلاعات باید هر بار به این فایل Append و اضافه شوند. سطرهای بعدی مشخص میکنند که هر زمانیکه این لاگ فایل به 10 مگابایت رسید، یک فایل جدید تولید کن و هر بار 10 فایل آخر را نگه دار و مابقی فایلهای قدیمی را حذف کن.
در قسمت PatternLayout مشخصات میکنیم که خروجی ثبت شده با چه فرمتی باشد. برای مثال یک سطر خروجی مطابق با تنظیمات فوق به شکل زیر خواهد بود:
2009-10-18 20:03:54,187 DEBUG INSERT INTO [Student] (Name) VALUES (@p0); select SCOPE_IDENTITY();@p0 = 'Vahid'
توسط appender-ref آن appender ایی را که در ابتدای کار تعریف و تنظیم کردیم، مشخص خواهیم کرد.
اگر هم با برنامه نویسی بخواهیم اطلاعاتی را به این لاگ فایل اضافه کنیم تنها کافی است بنویسیم:
log4net.LogManager.GetLogger("NHibernate.SQL").Info("test1");
اطلاعات بیشتر
ادامه دارد ...
محدودیتهای کار با اشیاء COM در NET Core 2x.
پیاده سازی پشتیبانی از اشیاء COM در NET Core 2x. به همراه اینترفیس IDispatch نیست. به این معنا که از مفهوم «late binding» پشتیبانی نمیکند. حدود 10 سال قبل در زمان ارائهی C# 4.0، واژهی کلیدی dynamic نیز ارائه شد که یکی از مهمترین اهداف آن، ساده سازی کار با اشیاء COM و پشتیبانی از Late binding بود:
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application", true)); excel.Visible = true; Console.WriteLine("Press Enter to close Excel."); Console.ReadLine(); excel.Quit();
System.__ComObject does not contain a definition for 'Visible'
یک نکته: NET Core 3x. از Late binding پشتیبانی میکند.
روش کار با اشیاء COM در NET Core 2x.
چون NET Core 2x. از late binding اشیاء COM پشتیبانی نمیکند، میتوان در اینجا از روش قدیمیتر کار با اشیاء COM که استفادهی از «Interop assemblies» نام دارد، استفاده کرد. Interop assemblies در حقیقت محصور کنندههای اشیاء COM هستند که امکان کار مستقیم با آنها را از طریق early binding میسر میکنند. در یک چنین حالتی، کدهای فوق برای دسترسی به اشیاء COM کار با اکسل، به صورت زیر که early binding نام دارد، تغییر میکند:
using Excel = Microsoft.Office.Interop.Excel; // ... var excel = new Excel.Application(); excel.Visible = true; Console.WriteLine("Press Enter to close Excel."); Console.ReadLine(); excel.Quit();
روش تولید Interop assemblies
هنوز خود NET Core. روشی را برای تولید Interop assemblies ارائه ندادهاست و تولید آنها یکی از معدود مواردی است که نیاز به نصب Visual Studio را دارد. برای این منظور یک پروژهی خالی (از هر نوعی) را که بر اساس NET Framework 4x. تهیه میشود، در VS آغاز کنید و سپس در solution explorer بر روی پروژهی ایجاد شده کلیک راست کرده و گزینهی Add > Reference را انتخاب کنید. در صفحهی باز شده، گزینهی COM آنرا باید انتخاب کنید. در اینجا است که میتوانید با انتخاب یکی از موارد، ارجاعی را به آن شیء COM اضافه کنید.
پس از اینکار:
- ابتدا این ارجاع اضافه شده را در solution explorer انتخاب کرده و در پایین صفحه، در قسمت برگهی خواص آن، گزینهی «Embed Interop Types» آنرا به false تنظیم کنید.
- سپس یکبار پروژه را نیز کامپایل کنید.
این مراحل سبب تولید یک فایل dll خواهند شد که Interop assembly نام دارد و هم در برنامههای NET. و هم NET Core.، قابل استفادهاست.
روش استفاده از Interop assemblies در برنامههای NET Core.
اکنون که یک فایل dll را از شیء COM انتخابی، در یک پروژهی مجزای مبتنی بر NET 4x. تولید کردیم، روش استفادهی از آن در یک برنامهی دیگر مبتنی بر NET Core. به صورت زیر است:
<ItemGroup> <Reference Include="Interop.WIA"> <HintPath>..\DNTScanner.Core.TypeLibrary\bin\Debug\Interop.WIA.dll</HintPath> <EmbedInteropTypes>True</EmbedInteropTypes> </Reference> </ItemGroup>
یک نکته: اگر EmbedInteropTypes را به true تنظیم کردید، نیاز به بستهی Microsoft.CSharp را نیز خواهید داشت:
<ItemGroup Condition=" '$(TargetFramework)' == 'net40' "> <Reference Include="Microsoft.CSharp" /> </ItemGroup> <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'"> <PackageReference Include="Microsoft.CSharp" Version="4.5.0" /> </ItemGroup>
روش دیگر استفاده از Interop assemblies در برنامههای NET Core.
روش فوق، جهت کار با فایلهای dll ای است که خودمان تولید کردهایم. برای سایر حالاتی که این موارد در سیستم نصب شدهاند (مانند Office Primary Interop Assemblies (PIA))، پس از افزودن ارجاعی به COM reference مدنظر، فایل csproj همان پروژهی NET 4x. را باز کرده و قسمت COMReference آنرا در اینجا (در فایل csproj پروژهی NET Core.) کپی کنید:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> </PropertyGroup> <!-- The following 'COMReference' items were copied from a .NET Framework project. They were added by using the Visual Studio COM References window. See https://docs.microsoft.com/en-us/visualstudio/ide/managing-references-in-a-project?view=vs-2017. Observe the 'EmbedInteropTypes' tag value. See https://docs.microsoft.com/en-us/visualstudio/msbuild/common-msbuild-project-items?view=vs-2017#comreference --> <ItemGroup> <COMReference Include="Microsoft.Office.Core"> <Guid>{2DF8D04C-5BFA-101B-BDE5-00AA0044DE52}</Guid> <VersionMajor>2</VersionMajor> <VersionMinor>8</VersionMinor> <Lcid>0</Lcid> <WrapperTool>primary</WrapperTool> <Isolated>False</Isolated> <EmbedInteropTypes>True</EmbedInteropTypes> </COMReference> <COMReference Include="Microsoft.Office.Interop.Excel"> <Guid>{00020813-0000-0000-C000-000000000046}</Guid> <VersionMajor>1</VersionMajor> <VersionMinor>9</VersionMinor> <Lcid>0</Lcid> <WrapperTool>primary</WrapperTool> <Isolated>False</Isolated> <EmbedInteropTypes>True</EmbedInteropTypes> </COMReference> <COMReference Include="VBIDE"> <Guid>{0002E157-0000-0000-C000-000000000046}</Guid> <VersionMajor>5</VersionMajor> <VersionMinor>3</VersionMinor> <Lcid>0</Lcid> <WrapperTool>primary</WrapperTool> <Isolated>False</Isolated> <EmbedInteropTypes>True</EmbedInteropTypes> </COMReference> </ItemGroup> </Project>
سپس یک نمونه از MS Office automation را توسط اشیاء COM آن به صورت زیر میتوان پیاده سازی کرد:
using System; using System.Reflection; using Excel = Microsoft.Office.Interop.Excel; namespace ExcelDemo { class Program { public static void Main(string[] args) { Excel.Application excel; Excel.Workbook workbook; Excel.Worksheet sheet; Excel.Range range; try { // Start Excel and get Application object. excel = new Excel.Application(); excel.Visible = true; // Get a new workbook. workbook = excel.Workbooks.Add(Missing.Value); sheet = (Excel.Worksheet)workbook.ActiveSheet; // Add table headers going cell by cell. sheet.Cells[1, 1] = "First Name"; sheet.Cells[1, 2] = "Last Name"; sheet.Cells[1, 3] = "Full Name"; sheet.Cells[1, 4] = "Salary"; // Format A1:D1 as bold, vertical alignment = center. sheet.get_Range("A1", "D1").Font.Bold = true; sheet.get_Range("A1", "D1").VerticalAlignment = Excel.XlVAlign.xlVAlignCenter; // Create an array to multiple values at once. string[,] saNames = new string[5, 2]; saNames[0, 0] = "John"; saNames[0, 1] = "Smith"; saNames[1, 0] = "Tom"; saNames[1, 1] = "Brown"; saNames[2, 0] = "Sue"; saNames[2, 1] = "Thomas"; saNames[3, 0] = "Jane"; saNames[3, 1] = "Jones"; saNames[4, 0] = "Adam"; saNames[4, 1] = "Johnson"; // Fill A2:B6 with an array of values (First and Last Names). sheet.get_Range("A2", "B6").Value2 = saNames; // Fill C2:C6 with a relative formula (=A2 & " " & B2). range = sheet.get_Range("C2", "C6"); range.Formula = "=A2 & \" \" & B2"; // Fill D2:D6 with a formula(=RAND()*100000) and apply format. range = sheet.get_Range("D2", "D6"); range.Formula = "=RAND()*100000"; range.NumberFormat = "$0.00"; // AutoFit columns A:D. range = sheet.get_Range("A1", "D1"); range.EntireColumn.AutoFit(); // Make sure Excel is visible and give the user control // of Microsoft Excel's lifetime. excel.Visible = true; excel.UserControl = true; } catch (Exception e) { Console.WriteLine($"Error: {e.Message} Line: {e.Source}"); } } } }
How to automate Microsoft Excel from Microsoft Visual C#.NET
آسیب پذیری SQL Injection یا به اختصار SQLi
تزریق SQL، یکی از قدیمی ترین، شایعترین و مخربترین آسیب پذیریها، برای برنامهها میباشد و در صورت برقراری شرایط مناسب جهت حمله و با اعمال نفوذ، از طریق تزریق SQL ، مهاجم میتواند با دور زدن فرآیندهای اعتبارسنجی و احراز هویت یک برنامه، به تمامی محتوای پایگاه دادهی آن و گاها کنترل سرور، دسترسی پیدا کند. این حمله برای افزودن، ویرایش و حذف رکوردهای یک پایگاه داده مبتنی بر SQL انجام میشود.
عملکرد SQL Injection
برای اجرای SQLهای مخرب در برنامههایی که از پایگاههای دادهی مبتنی بر SQL مانند (SQL Server ،MySQL ،PostgreSQL ،Oracle و ...) استفاده میکنند، هکر یا مهاجم در اولین گام باید به دنبال ورودیهایی در برنامه باشد که درون یک درخواست SQL قرار گرفته باشند (مانند صفحات لاگین، ثبت نام، جستجو و ...).
کد زیر را در نظر بگیرید:
# Define POST variables uname = request.POST['username'] passwd = request.POST['password'] # SQL query vulnerable to SQLi sql = "SELECT id FROM users WHERE username='" + uname + "' AND password='" + passwd + "'" # Execute the SQL statement database.execute(sql)
اکنون ورودی password را برای نفوذ، تست میکنیم. مهاجم بدون داشتن نام کاربری، قصد دور زدن احراز هویت را دارد. بجای password عبارت زیر را قرار میدهد:
password' OR 1=1
در نهایت در بانک اطلاعاتی دستور زیر اجرا میشود:
SELECT id FROM users WHERE username='username' AND password= 'password' OR 1=1'
میدانیم که 1=1 است. پس بدون در نظر گرفتن اینکه شما برای username و password چه چیزی را وارد نمودید، عبارت درست در نظر گرفته میشود:
شرط اول and شرط دوم = نتیجه or 1=1 چون 1=1 است همیشه شرط کوئری درست خواهد بود
معمولا در بانک اطلاعاتی، اولین کاربری که وارد میکنند Administrator برنامه میباشد. پس به احتمال قوی شما میتوانید با مجوز ادمین به برنامه وارد شوید. البته میتوان با دانستن تنها نام کاربری هم بهراحتی با گذاشتن در قسمت username بدون دانستن password، به برنامه وارد شد؛ زیرا میتوان شرط چک کردن password را کامنت نمود:
-- MySQL, MSSQL, Oracle, PostgreSQL, SQLite ' OR '1'='1' -- ' OR '1'='1' /* -- MySQL ' OR '1'='1' # -- Access (using null characters) ' OR '1'='1' %00 ' OR '1'='1' %16
ابزارهایی برای تست آسیب پذیری SQLi
2) اسکنر اکانتیکس
چگونه از SQL Injection جلوگیری کنیم
1) روی دادههایی که از کاربر دریافت میگردد، اعتبار سنجی سمت کلاینت و سرور انجام شود. اگر فقط به اعتبارسنجی سمت کلاینت اکتفا کنید، هکر بهراحتی با استفاده از پروکسی، دادهها را تغییر میدهد. ورودیها را فیلتر و پاکسازی و با لیست سفید یا سیاه بررسی کنید ( ^ , ^, ^, ^ ).
2) از کوئریهایی که بدون استفاده از پارامتر از کاربر ورودی گرفته و درون یک درخواستSQL قرار میگیرند، اجتناب کنید:
[HttpGet] [Route("nonsensitive")] public string GetNonSensitiveDataById() { using (SqlConnection connection = new SqlConnection(_configuration.GetValue<string>("ConnectionString"))) { connection.Open(); SqlCommand command = new SqlCommand($"SELECT * FROM NonSensitiveDataTable WHERE Id = {Request.Query["id"]}", connection); using (var reader = command.ExecuteReader()) { if (reader.Read()) { string returnString = string.Empty; returnString += $"Name : {reader["Name"]}. "; returnString += $"Description : {reader["Description"]}"; return returnString; } else { return string.Empty; } } } }
با استفاده از پارامتر: (بهتر است نوع دیتا تایپ پارامتر و طول آن ذکر شود)
[HttpGet] [Route("nonsensitivewithparam")] public string GetNonSensitiveDataByNameWithParam() { using (SqlConnection connection = new SqlConnection(_configuration.GetValue<string>("ConnectionString"))) { connection.Open(); SqlCommand command = new SqlCommand($"SELECT * FROM NonSensitiveDataTable WHERE Name = @name", connection); command.Parameters.AddWithValue("@name", Request.Query["name"].ToString()); using (var reader = command.ExecuteReader()) { if (reader.Read()) { string returnString = string.Empty; returnString += $"Name : {reader["Name"]}. "; returnString += $"Description : {reader["Description"]}"; return returnString; } else { return string.Empty; } } } }
3) از Stored Procedureها استفاده کنید و بصورت پارامتری دادههای مورد نیاز را به آنها پاس دهید: (بهتر است نوع دیتا تایپ پارامتر و طول آن ذکر شود)
[HttpGet] [Route("nonsensitivewithsp")] public string GetNonSensitiveDataByNameWithSP() { using (SqlConnection connection = new SqlConnection(_configuration.GetValue<string>("ConnectionString"))) { connection.Open(); SqlCommand command = new SqlCommand("SP_GetNonSensitiveDataByName", connection); command.CommandType = System.Data.CommandType.StoredProcedure; command.Parameters.AddWithValue("@name", Request.Query["name"].ToString()); using (var reader = command.ExecuteReader()) { if (reader.Read()) { string returnString = string.Empty; returnString += $"Name : {reader["Name"]}. "; returnString += $"Description : {reader["Description"]}"; return returnString; } else { return string.Empty; } } } }
4) اگر از داینامیک کوئری استفاده میکنید، دادههای مورد استفادهی در کوئری را بصورت پارامتری ارسال کنید:
فرض کنید چنین جدولی دارید
CREATE TABLE tbl_Product ( Name NVARCHAR(50), Qty INT, Price FLOAT ) GO INSERT INTO tbl_Product (Name, Qty, Price) VALUES (N'Shampoo', 200, 10.0); INSERT INTO tbl_Product (Name, Qty, Price) VALUES (N'Hair Clay', 400, 20.0); INSERT INTO tbl_Product (Name, Qty, Price) VALUES (N'Hair Tonic', 300, 30.0);
یک پروسیجر را دارید که عملیات جستجو را انجام میدهد و از داینامیک کوئری استفاده میکند.
ALTER PROCEDURE sp_GetProduct(@Name NVARCHAR(50)) AS BEGIN DECLARE @sqlcmd NVARCHAR(MAX); SET @sqlcmd = N'SELECT * FROM tbl_Product WHERE Name = ''' + @Name + ''''; EXECUTE(@sqlcmd) END
با اینکه از Stored Procedure استفاده میکنید، باز هم در معرض خطر SQLi میباشید. فرض کنید هکر چنین درخواستی را ارسال میکند:
Shampoo'; DROP TABLE tbl_Product; --
نتیجه، تبدیل به دستور زیر میشود:
SELECT * FROM tbl_Product WHERE Name = 'Shampoo'; DROP TABLE tbl_Product; --'
برای جلوگیری از SQLi در کوئریهای داینامیک SP بشکل زیر عمل میکنیم:
ALTER PROCEDURE sp_GetProduct(@Name NVARCHAR(50)) AS BEGIN DECLARE @sqlcmd NVARCHAR(MAX); DECLARE @params NVARCHAR(MAX); SET @sqlcmd = N'SELECT * FROM tbl_Product WHERE Name = @Name'; SET @params = N'@Name NVARCHAR(50)'; EXECUTE sp_executesql @sqlcmd, @params, @Name; END
5) میتوان از تنظیمات IIS یا وب سرورهای دیگر برای جلوگیری از SQLi استفاده نمود.
6) استفاده از چند کاربرِ دیتابیس در برنامه و بکارگیری سطح دسترسی محدود و مناسب( ^ , ^ ).
7) از ORM استفاده کنید و اگر نیاز به سرعت بیشتری دارید از یک Micro ORM استفاده کنید؛ با در نظر داشتن نکات لازم.
آموزش عالی برای کسانی که میخواهند WPF و الگوی MVVM و CODE FIRST رو باهم یاد بگیرند :
At the core of developing a data-driven WPF application is a thorough knowledge of how to use the MVVM Pattern and Entity Framework. In this course, Building an Enterprise App with WPF, MVVM, and Entity Framework Code First, you will learn the skills you need to create high-quality enterprise applications with WPF. First, you'll learn about typical scenarios like communicating between different ViewModels, detecting model changes, and handling many-to-many relations. Next, you'll learn all about creating a tabbed user interface. Finally, you'll explore implementing optimistic concurrency and styling your application. When you're finished with this course, you will have a deep understanding of WPF, MVVM, and Entity Framework that will help you immensely as you move forward and create your own data-driven enterprise application
مثال 1: یافتن زمانهای شروع رزرو کردن امکانات مختلف، توسط یک کاربر مشخص.
چگونه میتوان زمانهای شروع رزروهای کاربری به نام «David Farrell» را یافت؟
همانطور که در دیاگرام فوق مشاهده میکنید، به ازای هر ID کاربری در جدول کاربران، به دنبال ردیفهایی در جدول Bookings هستیم که این ID در آنها درج شدهاست. اما ... در EF-Core برخلاف SQL نویسی معمولی، ما کاری به ذکر قسمت اتصالی ON [Bookings].[MemId] = [Members].[MemId] نداریم. همینقدر که در کوئری نوشته شده به یک سر دیگر رابطه و خاصیت راهبری (navigation property) دیگری اشاره شود، خود EF-Core جوینی را به صورت خودکار تشکیل خواهد داد و شرط یاد شده را نیز برقرار میکند.
در قسمت اول این سری، در حین طراحی موجودیت کاربر، برای تشکیل سر دیگر رابطهی one-to-many آن، به جدول Bookings، خاصیت Member را نیز که بیانگر کلید خارجی به جدول کاربران است، اضافه کردیم:
namespace EFCorePgExercises.Entities { public class Booking { // ... public int MemId { set; get; } public virtual Member Member { set; get; } // ... } }
var startTimes = context.Bookings .Where(booking => booking.Member.FirstName == "David" && booking.Member.Surname == "Farrell") .Select(booking => new { booking.StartTime }) .ToList();
مثال 2: یافتن زمانهای شروع به رزرو شدن یک امکان خاص در مجموعه.
لیست زمانهای شروع به رزرو شدن زمین(های) تنیس را برای روز 2012-09-21 تولید کنید. خروجی آن باید به همراه ستونهای StartTime, FacilityName باشد.
طراحی موجودیت Booking، به همراه یک کلید خارجی به Facility نیز هست:
namespace EFCorePgExercises.Entities { public class Booking { // ... public int FacId { set; get; } public virtual Facility Facility { set; get; } // ... } }
int[] tennisCourts = { 0, 1 }; var date1 = new DateTime(2012, 09, 21); var date2 = new DateTime(2012, 09, 22); var startTimes = context.Bookings .Where(booking => tennisCourts.Contains(booking.Facility.FacId) && booking.StartTime >= date1 && booking.StartTime < date2) .Select(booking => new { booking.StartTime, booking.Facility.Name }) .ToList();
- در قسمت Where این کوئری چون booking.Facility ذکر شده، سبب ایجاد جوین خودکاری به جدول Facilities خواهد شد.
- علت استفادهی از دو تاریخ در اینجا برای یافتن اطلاعات تنها یک روز، ثبت زمان، به همراه تاریخ رزرو است. ستون تاریخ شروع، به صورت «2012-09-21 18:00:00.0000000» مقدار دهی شدهاست و نه به صورت «2012-09-21». البته در EF-Core راه دیگری هم برای حل این مساله وجود دارد. هر خاصیت از نوع DateTime، به همراه خاصیت Date نیز هست. برای مثال اگر بجای booking.StartTime نوشته شود booking.StartTime.Date (به خاصیت Date اضافه شده دقت کنید)، کد SQL حاصل، به همراه «CONVERT(date, [b].[StartTime])» خواهد بود که سبب حذف خودکار قسمت زمان این ستون میشود.
مثال 3: تولید لیست کاربرانی که کاربر دیگری را توصیه کردهاند.
چگونه میتوان لیست کاربرانی را یافت که کاربر دیگری را توصیه کردهاند؟ این لیست نباید به همراه ردیفهای تکراری باشد و همچنین باید بر اساس surname, firstname مرتب شود.
در اینجا به مفهوم جوین کردن یک جدول با خودش رسیدهایم. جدول کاربران، یک جدول خود ارجاع دهندهاست:
namespace EFCorePgExercises.Entities { public class Member { // ... public virtual ICollection<Member> Children { get; set; } public virtual Member Recommender { set; get; } public int? RecommendedBy { set; get; } // ... } }
var members = context.Members .Where(member => member.Recommender != null) .Select(member => new { member.Recommender.FirstName, member.Recommender.Surname }) .Distinct() .OrderBy(member => member.Surname).ThenBy(member => member.FirstName) .ToList();
مثال 4: تولید لیست کاربران به همراه توصیه کنندهی آنها.
چگونه میتوان لیست کاربران را به همراه توصیه کنندهی آنها تولید کرد؟ این لیست باید بر اساس surname, firstname مرتب شود.
var members = context.Members .Select(member => new { memFName = member.FirstName, memSName = member.Surname, recFName = member.Recommender.FirstName ?? "", recSName = member.Recommender.Surname ?? "" }) .OrderBy(member => member.memSName).ThenBy(member => member.memFName) .ToList();
همانطور که ملاحظه میکنید، نوع جوین خودکار تشکیل شده، Left join است و دیگر مانند جوینهای مثالهای ابتدای بحث، inner join نیست. در inner join، جدول سمت راست و چپ بر اساس شرط ON آنها با هم مقایسه شده و ردیفهای کاملا تطابق یافتهای بازگشت داده میشوند. کار Left join نیز مشابه است، با این تفاوت که در اینجا ممکن است برای جدول سمت چپ، هیچ ردیف تطابق یافتهای در جدول سمت راست وجود نداشته باشد (نوع آن بر اساس نال پذیری خاصیت RecommendedBy تشخیص داده شدهاست)؛ برای مثال یک کاربر ممکن است توسط کاربر دیگری توصیه نشده باشد (و RecommendedBy او نال باشد)، اما علاقمندیم که نام او در لیست نهایی حضور داشته باشد و حذف نشود.
یک نکته: در SQL Server تفاوتی بین left join و left outer join وجود ندارد و ذکر واژهی کلیدی outer کاملا اختیاری است. جدول موارد مشابهی در SQL Server که به یک معنا هستند، صورت زیر است:
A LEFT JOIN B A LEFT OUTER JOIN B A RIGHT JOIN B A RIGHT OUTER JOIN B A FULL JOIN B A FULL OUTER JOIN B A INNER JOIN B A JOIN B
مثال 5: تولید لیست کاربرانی که از زمین تنیس استفاده کردهاند.
چگونه میتوان لیست کاربرانی را تولید کرد که از زمین(های) تنیس استفاده کردهاند؟ خروجی این گزارش باید به همراه یک ستون جمع نام و نام خانوادگی و ستون نام زمین باشد. این گزارش نباید دارای ردیفهای تکراری باشد و همچنین باید بر اساس حاصل جمع نام و نام خانوادگی، مرتب شده باشد.
جدول Bookings به همراه دو کلید خارجی به جداول Facilities و Members است:
namespace EFCorePgExercises.Entities { public class Booking { // ... public int FacId { set; get; } public virtual Facility Facility { set; get; } public int MemId { set; get; } public virtual Member Member { set; get; } // ... } }
namespace EFCorePgExercises.Entities { public class Member { // ... public virtual ICollection<Booking> Bookings { set; get; } } }
int[] tennisCourts = { 0, 1 }; var members = context.Members .SelectMany(x => x.Bookings) .Where(booking => tennisCourts.Contains(booking.Facility.FacId)) .Select(booking => new { Member = booking.Member.FirstName + " " + booking.Member.Surname, Facility = booking.Facility.Name }) .Distinct() .OrderBy(x => x.Member) .ToList();
پس از آن برای حذف ردیفهای تکراری حاصل از گزارش، از متد Distinct استفاده شده و OrderBy نیز بر اساس خاصیت جدید Member، قابل تعریف است:
مثال 6: تولید لیست رزروهای گران قیمت
لیست رزروهای روز 2012-09-14 را تولید کنید که هزینهی آنها بیشتر از 30 دلار باشد. باید بخاطر داشت که هزینههای کاربران با مهمانها متفاوت است و هزینهها بر اساس Slotهای نیم ساعته محاسبه میشوند و ID کاربر مهمان همیشه صفر است. خروجی این گزارش باید به همراه نام کامل کاربر، نام امکانات مورد استفاده و هزینهی نهایی باشد. همچنین باید بر اساس هزینههای نهایی به صورت نزولی مرتب شود.
var date1 = new DateTime(2012, 09, 14); var date2 = new DateTime(2012, 09, 15); var items = context.Members .SelectMany(x => x.Bookings) .Where(booking => booking.StartTime >= date1 && booking.StartTime < date2 && ( (((booking.Slots * booking.Facility.GuestCost) > 30) && (booking.MemId == 0)) || (((booking.Slots * booking.Facility.MemberCost) > 30) && (booking.MemId != 0)) )) .Select(booking => new { Member = booking.Member.FirstName + " " + booking.Member.Surname, Facility = booking.Facility.Name, Cost = booking.MemId == 0 ? booking.Slots * booking.Facility.GuestCost : booking.Slots * booking.Facility.MemberCost }) .Distinct() .OrderByDescending(x => x.Cost) .ToList();
سپس بر اساس صفر بودن یا نبودن booking.MemId (کاربر مهمان بودن یا خیر)، شرط هزینهی بیشتر از 30 دلار اعمال شدهاست.
در آخر Select گزارش مورد نیاز، به همراه جمع نام و نام خانوادگی، نام امکانات استفاده شده و خاصیت محاسباتی Cost است که بر اساس مهمان بودن یا نبودن کاربر، متفاوت است.
متد Distinct ردیفهای تکراری حاصل از این گزارش را حذف میکند (محل درج آن مهم است) و متد OrderByDescending، مرتب سازی نزولی بر اساس خاصیت محاسباتی Cost را انجام میدهد.
مثال 7: تولید لیست کاربران به همراه توصیه کنندهی آنها، بدون استفاده از جوین.
در اینجا میخواهیم همان مثال 4 را بدون استفاده از جوین بررسی کنیم. بدون استفاده از جوین در اینجا به معنای استفاده از sub-query است (نوشتن یک کوئری داخل کوئری اصلی).
var members = context.Members .Select(member => new { Member = member.FirstName + " " + member.Surname, Recommender = context.Members .Where(recommender => recommender.MemId == member.RecommendedBy) .Select(recommender => recommender.FirstName + " " + recommender.Surname) .FirstOrDefault() ?? "" }) .Distinct() .OrderBy(member => member.Member) .ToList();
مثال 8: تولید لیست رزروهای گران قیمت با استفاده از یک sub-query.
هدف از این مثال، ارائهی روش حل دیگری برای مثال 6، به نحو تمیزتری است. در مثال 6، هزینهی رزرو را دوبار، یکبار در متد Where و یکبار در متد Select محاسبه کردیم. اینبار میخواهیم با استفاده از sub-queryها این محاسبه را یکبار انجام دهیم.
var date1 = new DateTime(2012, 09, 14); var date2 = new DateTime(2012, 09, 15); var items = context.Members .SelectMany(x => x.Bookings) .Where(booking => booking.StartTime >= date1 && booking.StartTime < date2) .Select(booking => new { Member = booking.Member.FirstName + " " + booking.Member.Surname, Facility = booking.Facility.Name, Cost = booking.MemId == 0 ? booking.Slots * booking.Facility.GuestCost : booking.Slots * booking.Facility.MemberCost }) .Where(x => x.Cost > 30) .Distinct() .OrderByDescending(x => x.Cost) .ToList();
هرچند کوئری SQL نهایی تولید شدهی توسط EF-Core آن، تفاوتی چندانی با نگارش قبلی ندارد:
کدهای کامل این قسمت را در اینجا میتوانید مشاهده کنید.
تنظیم خروجی متدهای اشیاء Mock شده
در انتهای قسمت قبل، آزمون واحد متد Accept، با شکست مواجه شد؛ چون متد Validate استفاده شده، همواره مقدار false را بر میگرداند:
_identityVerifier.Initialize(); var isValidIdentity = _identityVerifier.Validate( application.Applicant.Name, application.Applicant.Age, application.Applicant.Address);
در ادامه شیء Mock از نوع IIdentityVerifier را طوری تنظیم خواهیم کرد که بر اساس یک applicant مشخص، خروجی true را بازگشت دهد:
namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void Accept() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_500_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var mockIdentityVerifier = new Mock<IIdentityVerifier>(); mockIdentityVerifier.Setup(x => x.Validate(applicant.Name, applicant.Age, applicant.Address)) .Returns(true); var mockCreditScorer = new Mock<ICreditScorer>(); var processor = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object); processor.Process(application); Assert.IsTrue(application.IsAccepted); } } }
به این ترتیب زمانیکه در متد Process کلاس LoanApplicationProcessor کار به بررسی هویت کاربر میرسد، اگر متد Validate آن با اطلاعات applicant مشخصی که تنظیم کردیم، یکی بود، متغیر isValidIdentity که حاصل بررسی identityVerifier.Validate_ است، به true مقدار دهی خواهد شد. برای بررسی آن یک break-point را در این نقطه قرار داده و آزمون واحد را در حالت دیباگ اجرا کنید.
البته هرچند اگر اکنون نیز این آزمایش واحد را مجددا بررسی کنیم، باز هم با شکست مواجه خواهد شد؛ چون مرحلهی بعدی بررسی، کار با سرویس ICreditScorer است که هنوز تنظیم نشدهاست:
_creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address); if (_creditScorer.Score < MinimumCreditScore) { return application.IsAccepted; }
تطابق با آرگومانهای متدها در متدهای Mock شده
با تنظیمی که انجام دادیم، اگر متد Validate به مشخصات شیء applicant مشخص ما برسد، خروجی true را بازگشت میدهد. برای مثال اگر در این بین تنها نام شخص تغییر کند، خروجی بازگشت داده شده همان false خواهد بود. اما اگر این نام برای ما اهمیتی نداشت و قصد داشتیم با تمام نامهای متفاوتی که دریافت میکند، بازهم خروجی true را بازگشت دهد، میتوان از قابلیت argument matching کتابخانهی Moq و کلاس It آن استفاده کرد:
var mockIdentityVerifier = new Mock<IIdentityVerifier>(); mockIdentityVerifier.Setup(x => x.Validate( //applicant.Name, It.IsAny<string>(), applicant.Age, applicant.Address)) .Returns(true);
بدیهی است در این حالت باید سایر پارامترها دقیقا با مقادیر مشخص شده تطابق داشته باشند و اگر این موارد نیز اهمیتی نداشتند، میتوان به صورت زیر عمل کرد:
var mockIdentityVerifier = new Mock<IIdentityVerifier>(); mockIdentityVerifier.Setup(x => x.Validate( //applicant.Name, It.IsAny<string>(), //applicant.Age, It.IsAny<int>(), //applicant.Address It.IsAny<string>() )) .Returns(true);
البته این نوع تنظیمات بیشتر برای حالات غیرمشخص مانند استفادهاز Guidها به عنوان پارامترها و مقادیر، میتواند مفید باشد.
تقلید متدهایی که پارامترهایی از نوع out دارند
اگر به اینترفیس IIdentityVerifier که در قسمت قبل معرفی شد دقت کنیم، یکی از متدهای آن دارای خروجی از نوع out است:
using Loans.Models; namespace Loans.Services.Contracts { public interface IIdentityVerifier { void Validate(string applicantName, int applicantAge, string applicantAddress, out bool isValid); // ... } }
//var isValidIdentity = _identityVerifier.Validate( // application.Applicant.Name, application.Applicant.Age, application.Applicant.Address); _identityVerifier.Validate( application.Applicant.Name, application.Applicant.Age, application.Applicant.Address, out var isValidIdentity);
var isValidOutValue = true; mockIdentityVerifier.Setup(x => x.Validate(applicant.Name, applicant.Age, applicant.Address, out isValidOutValue));
اکنون اگر مجددا آزمون واحد متد Accept را اجرا کنیم، با موفقیت به پایان میرسد.
تقلید متدهایی که پارامترهایی از نوع ref دارند
اگر به اینترفیس IIdentityVerifier که در قسمت قبل معرفی شد دقت کنیم، یکی از متدهای آن دارای خروجی از نوع ref است:
using Loans.Models; namespace Loans.Services.Contracts { public interface IIdentityVerifier { void Validate(string applicantName, int applicantAge, string applicantAddress, ref IdentityVerificationStatus status); // ... } }
namespace Loans.Models { public class IdentityVerificationStatus { public bool Passed { get; set; } } }
IdentityVerificationStatus status = null; _identityVerifier.Validate( application.Applicant.Name, application.Applicant.Age, application.Applicant.Address, ref status); if (!status.Passed) { return application.IsAccepted; }
ابتدا در سطح کلاس آزمایش واحد یک delegate را تعریف میکنیم:
delegate void ValidateCallback(string applicantName, int applicantAge, string applicantAddress, ref IdentityVerificationStatus status);
اکنون روش استفادهی از آن برای برپایی تنظیمات mocking متد Validate از نوع ref دار به صورت زیر است:
mockIdentityVerifier .Setup(x => x.Validate(applicant.Name, applicant.Age, applicant.Address, ref It.Ref<IdentityVerificationStatus>.IsAny)) .Callback(new ValidateCallback( (string applicantName, int applicantAge, string applicantAddress, ref IdentityVerificationStatus status) => status = new IdentityVerificationStatus {Passed = true}));
سپس متد جدید Callback را مشاهده میکنید. توسط آن میتوان یک قطعه کد سفارشی را زمانیکه متد Mock شدهی Validate ما اجرا میشود، اجرا کرد. در اینجا delegate سفارشی ما اجرا شده و مقدار status را بر میگرداند؛ اما در ادامه این مقدار را به یک new IdentityVerificationStatus سفارشی تنظیم میکنیم که در آن مقدار خاصیت Passed، مساوی true است.
اکنون اگر مجددا آزمون واحد متد Accept را اجرا کنیم، با موفقیت به پایان میرسد.
تنظیم متدهای Mock شده جهت بازگشت null
فرض کنید اینترفیسی به صورت زیر تعریف شدهاست:
namespace Loans.Services.Contracts { public interface INullExample { string SomeMethod(); } }
namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void NullReturnExample() { var mock = new Mock<INullExample>(); mock.Setup(x => x.SomeMethod()); //.Returns<string>(null); string mockReturnValue = mock.Object.SomeMethod(); Assert.IsNull(mockReturnValue); } } }
الف) میتوان همانند سابق متد Returns را ذکر کرد که نال بر میگرداند؛ اما با این تفاوت که حتما باید نوع آرگومان جنریک آنرا نیز بر اساس خروجی متد، مشخص کرد.
ب) کتابخانهی Moq، مقدار خروجی پیشفرض تمام متدهایی را که یک نوع ارجاعی را باز میگردانند، نال درنظر میگیرد و عملا نیازی به ذکر متد Returns در اینجا نیست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MoqSeries-02.zip