دوره مقدماتی NET 7.
.NET 7 Beginner Course 🚀 Web API, Entity Framework 7 & SQL Server
Table of Contents:
00:00:00 .NET 7 Beginner Course 🚀 Web API, Entity Framework 7 & SQL Server
00:01:18 Tools (Visual Studio Code & .NET SDK)
00:02:48 Create a new Web API
00:11:34 First API Call
00:15:23 Git Repository & .gitignore File
00:19:07 Web API Introduction
00:19:42 The Model-View-Controller (MVC) Pattern
00:22:03 New Models
00:26:17 New Controller & GET a New Character
00:36:35 First Steps with Attribute Routing
00:40:52 Routing with Parameters
00:43:34 HTTP Request Methods Explained
00:46:48 Add a New Character with POST
00:50:23 Best Practice: Web API Structure
00:53:42 Character Service
01:02:38 Fix the “Possible ArgumentNullException”
01:04:43 Asynchronous Calls
01:08:53 Proper Service Response with Generics
01:17:06 Data-Transfer-Objects (DTOs)
01:22:58 AutoMapper
01:35:30 Modify a Character with PUT
01:47:40 Modify a Character with AutoMapper
01:49:12 Delete a Character
01:54:15 Web API Summary
01:55:01 Entity Framework 7 Introduction
01:55:50 Object-Relational-Mapping & Code-First Migration Explained
01:57:42 Installing Entity Framework 7
02:00:48 Installing SQL Server Express (with Management Studio)
02:02:04 Implementing the DataContext
02:05:37 ConnectionString & Adding the DbContext
02:10:29 First Migration
02:14:49 GET Implementations
در این نوشتار که به صورت آموزش تصویری ارائه میشود؛ یک سرویس WCF در Visual Studio 2013 ایجاد میکنم، سپس روش استفاده از آنرا در یک برنامه ویندوزی آموزش خواهم داد. در اینجا در نظرگرفته شده است که شما افزونهی Resharper را روی ویژوال استودیوی خود نصب دارید. پس در صورتیکه هنوز به سراغ آن نرفته اید درنگ نکنید و واپسین نگارش آن را دانلود کنید.
در این پروژهی ساده در نظر میگیریم که دو جدول یکی برای اخبار، شامل عنوان، متن خبر و تاریخ ثبت و دسته بندی و دیگری برای نگهداری دستهها در پایگاه داده داریم و میخواهیم سرویسهای مناسب با این دو جدول را بسازیم. با کد زیر، پایگاه دادهی dbTest و جدولهای tblNews و tblCategory در SQL Server 2012 ساخته میشود:
USE [master] GO /****** Object: Database [dbMyNews] Script Date: 2014/01/14 09:46:04 ب.ظ ******/ CREATE DATABASE [dbMyNews] CONTAINMENT = NONE ON PRIMARY ( NAME = N'dbMyNews', FILENAME = N'D:\dbMyNews.mdf' , SIZE = 5120KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB ) LOG ON ( NAME = N'dbMyNews_log', FILENAME = N'D:\dbMyNews_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%) GO USE [dbMyNews] GO /****** Object: Table [dbo].[tblCategory] Script Date: 2014/01/14 09:46:04 ب.ظ ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[tblCategory]( [tblCategoryId] [int] IDENTITY(1,1) NOT NULL, [CatName] [nvarchar](50) NOT NULL, [IsDeleted] [bit] NOT NULL, CONSTRAINT [PK_tblCategory] PRIMARY KEY CLUSTERED ( [tblCategoryId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO /****** Object: Table [dbo].[tblNews] Script Date: 2014/01/14 09:46:04 ب.ظ ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[tblNews]( [tblNewsId] [int] IDENTITY(1,1) NOT NULL, [tblCategoryId] [int] NOT NULL, [Title] [nvarchar](50) NOT NULL, [Description] [nvarchar](max) NOT NULL, [RegDate] [datetime] NOT NULL, [IsDeleted] [bit] NULL, CONSTRAINT [PK_tblNews] PRIMARY KEY CLUSTERED ( [tblNewsId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO ALTER TABLE [dbo].[tblNews] WITH CHECK ADD CONSTRAINT [FK_tblNews_tblCategory] FOREIGN KEY([tblCategoryId]) REFERENCES [dbo].[tblCategory] ([tblCategoryId]) GO ALTER TABLE [dbo].[tblNews] CHECK CONSTRAINT [FK_tblNews_tblCategory] GO USE [master] GO ALTER DATABASE [dbMyNews] SET READ_WRITE GO
اکنون Visual Studio 2013 را بازکنید سپس روی گزینه New Project کلیک کنید و برابر با نگارهی زیر عمل کنید:
پروژه MyNewsWCFLibrary در راه حل MyNews ساخته میشود. این پروژه به صورت پیشگزیده دارای یک کلاس به نام Service و یک interface به نام IService است. هر دو را حذف کنید و سپس روی نام پروژه راستکلیک کرده، از منوی بازشده گزینهی Add -> New Item را انتخاب کنید. سپس برابر با نگارهی زیر عمل کنید:
در لایهی Service Interface کلیهی روالهای مورد نیاز برای ارتباط با پایگاه داده را میسازیم. پیش از آن باید یک Model برای ارتباط با پایگاه داده ساخته باشیم. برای این کار از پنجره Add New Item و از زیرمجموعه Data، گزینه ADO.NET Entity Data Model را انتخاب کنید و بهسان زیر پیش روید:
در گام پسین روی دکمه New Connection کلیک کنید و رشتهی اتصال به پایگاه دادهی dbMyNews را بسازید. سپس همانند تنظیمات نگارهی زیر ادامه دهید:
در گام پسین گزینهی Entity Framework 6.0 را برگزینید و روی دکمهی Next کلیک کنید.
در پنجره نشان داده شده، جدولهای مورد نیاز را همانند نگارهی زیر انتخاب کرده و روی دکمه Finish کلیک کنید:
در پایان مدل ما همانند نگارهی زیر خواهد بود.
در بخش پسین دربارهی شیوهی دستکاری کلاسهای Entity خواهم نوشت.
context.Chapters.Add(new Chapter { Title = "آزمایش متن فارسی", Text = "برای نمونه تهیه شدهاست", User = user1.Entity });
کامپایل افزونهی spell fix1
افزونهی spell fix، به همراه هیچکدام از توزیعهای باینری SQLite ارائه نمیشود. ارائهی آن فقط به صورت سورس کد است و باید خودتان آنرا کامپایل کنید!
برای این منظور ابتدا به آدرس https://www.sqlite.org/src/dir?ci=99749d4fd4930ccf&name=ext/misc مراجعه کرده و فایل ext/misc/spellfix.c آنرا دریافت کنید. اگر بر روی لینک spellfix.c کلیک کنید، در نوار ابزار بالای صفحهی بعدی، لینک download آن هم وجود دارد.
سپس به صفحهی دریافت اصلی SQLite یعنی https://www.sqlite.org/download.html مراجعه کرده و بستهی amalgamation آنرا دریافت کنید. این بسته به همراه کدهای اصلی SQLite است که باید در کنار افزونههای آن قرار گیرند تا بتوان این افزونهها را کامپایل کرد. بنابراین پس از دریافت بستهی amalgamation و گشودن آن، فایل spellfix.c را به داخل پوشهی آن کپی کنید:
اکنون نوبت به کامپایل فایل spellfix.c و تبدیل آن به یک dll است تا بتوان آنرا به صورت یک افزونه در برنامه بارگذاری کرد. برای این منظور از هر کامپایلر ++C ای میتوانید استفاده کنید. برای نمونه به آدرس http://www.codeblocks.org/downloads/binaries مراجعه کرده و بستهی codeblocks-20.03mingw-setup.exe را دریافت کنید (بستهای که به همراه mingw است). پس از نصب آن، در مسیر C:\Program Files (x86)\CodeBlocks\MinGW\bin میتوانید کامپایلر چندسکویی gcc را مشاهده کنید. توسط آن میتوان با اجرای دستور زیر، سبب تولید فایل spellfix1.dll شد:
"C:\Program Files (x86)\CodeBlocks\MinGW\bin\gcc.exe" -g -shared -fPIC -Wall D:\path\to\sqlite-amalgamation-3310100\spellfix.c -o spellfix1.dll
روش معرفی افزونههای SQLite به Microsoft.Data.Sqlite
EF Core، از بستهی Microsoft.Data.Sqlite در پشت صحنه برای کار با SQLite استفاده میکند و در اینجا هم برای معرفی افزونهی کامپایل شده، باید ابتدا آنرا به اتصال برقرار شده، معرفی کرد. خود Sqlite در ویندوز، افزونههایش را بر اساس معرفی مستقیم مسیر فایل dll آنها بارگذاری نمیکند. بلکه path ویندوز را برای جستجوی آنها بررسی کرده و در صورتیکه فایل dll ای را افزونه تشخیص داد، آنرا بارگذاری میکند. بنابراین یا باید به صورت دستی مسیر فایل dll تولید شده را به متغیر محیطی path ویندوز اضافه کرد و یا میتوان توسط قطعه کد زیر، آنرا به صورت پویایی معرفی کرد:
using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; namespace EFCoreSQLiteFTS.DataLayer { public static class LoadSqliteExtensions { public static void AddToSystemPath(string extensionsDirectory) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { throw new NotSupportedException("Modifying the path at runtime only works on Windows. On Linux and Mac, set LD_LIBRARY_PATH or DYLD_LIBRARY_PATH before running the app."); } var path = new HashSet<string>(Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator)); if (path.Add(extensionsDirectory)) { Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator, path)); } } } }
در ادامه پیش از معرفی services.AddDbContext، باید مسیر پوشهی افزونهها را ثبت کرد و سپس UseSqlite را به همراه اتصالی استفاده کرد که توسط متد LoadExtension آن، افزونهی spellfix1 به آن معرفی شدهاست:
LoadSqliteExtensions.AddToSystemPath("path to .dll file"); services.AddDbContext<ApplicationDbContext>((serviceProvider, optionsBuilder) => { var connection = new SqliteConnection(connectionString); connection.Open(); connection.LoadExtension("spellfix1"); // Passing in an already open connection will keep the connection open between requests. optionsBuilder.UseSqlite(connection); });
ایجاد جداول ویژهی spell fix در برنامه
در قسمت اول، با متد createFtsTables آشنا شدیم. اکنون این متد را برای ایجاد جداول کمکی مرتبط با افزونهی spell fix به صورت زیر تکمیل میکنیم:
private static void createFtsTables(ApplicationDbContext context) { // For SQLite FTS // Note: This can be added to the `protected override void Up(MigrationBuilder migrationBuilder)` method too. context.Database.ExecuteSqlRaw(@"CREATE VIRTUAL TABLE IF NOT EXISTS ""Chapters_FTS"" USING fts5(""Text"", ""Title"", content=""Chapters"", content_rowid=""Id"");"); // 'SQLite Error 1: 'no such module: spellfix1'.' --> must be loaded ... // EditCost for unicode support context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_Vocab USING fts5vocab('Chapters_FTS', 'row');"); context.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Chapters_FTS_SpellFix_EditCost(iLang INT, cFrom TEXT, cTo TEXT, iCost INT);"); context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_SpellFix USING spellfix1(edit_cost_table=Chapters_FTS_SpellFix_EditCost);"); }
- همانطور که مشاهده میکنید، ابتدا بر اساس Chapters_FTS یا همان جدول مجازی FTS برنامه، یک جدول مجازی از نوع fts5vocab ایجاد میشود. کار آن استخراج توکنهای FTS و آماده سازی آنها برای استفاده در غلط یاب املایی هستند.
- سپس جدول ویژهی EditCost را مشاهده میکنید. نام آن مهم نیست، اما ساختار آن باید دقیقا به همین صورت باشد. اگر این جدول اختیاری را تهیه کنیم، الگوریتم spellfix1 به utf8 سوئیچ خواهد کرد و برای پردازش متون یونیکد، بدون مشکل کار میکند. بدون آن، جستجوهای فارسی نتایج مطلوبی را به همراه نخواهند داشت.
- در آخر جدول مجازی مرتبط با spellfix1 که از جدول cost_table معرفی شده استفاده میکند، ایجاد شدهاست.
اجرای این دستورات، جداول زیر را ایجاد میکنند (که ساختار آنها استاندارد است و باید مطابق فرمولهای مستندات آنها باشد):
به روز رسانی جدول واژه نامهی غلط یابی برنامه
آخرین جدولی را که ایجاد کردیم، Chapters_FTS_SpellFix است که اطلاعات خودش را از Chapters_FTS_Vocab دریافت میکند:
هر بار که بانک اطلاعاتی را به روز میکنیم، نیاز است اطلاعات این جدول را نیز توسط دستور زیر به روز کرد:
database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS_SpellFix(word, rank) SELECT term, cnt FROM Chapters_FTS_Vocab WHERE term not in (SELECT word from Chapters_FTS_SpellFix_vocab)");
database.ExecuteSqlRaw("INSERT INTO Chapters_FTS_SpellFix(command) VALUES(\"reset\");");
کوئری گرفتن از جدول مجازی Chapters_FTS_SpellFix
تا اینجا افزونهی spellfix1 را کامپایل و به سیستم معرفی کردیم. سپس جداول واژه نامهی آنرا نیز تشکیل دادیم، اکنون نوبت به کوئری گرفتن از آن است. به همین جهت یک موجودیت بدون کلید دیگر را بر اساس ساختار خروجی کوئریهای آن ایجاد کرده:
namespace EFCoreSQLiteFTS.Entities { public class SpellCheck { public string Word { get; set; } public decimal Rank { get; set; } public decimal Distance { get; set; } public decimal Score { get; set; } public decimal Matchlen { get; set; } } }
namespace EFCoreSQLiteFTS.DataLayer { public class ApplicationDbContext : DbContext { //... protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<SpellCheck>().HasNoKey().ToView(null); } //... } }
در آخر، کوئری گرفتن از این جدول، ساختار زیر را دارد:
foreach (var item in context.Set<SpellCheck>().FromSqlRaw( @"SELECT word, rank, distance, score, matchlen FROM Chapters_FTS_SpellFix WHERE word MATCH {0} and top=6", "فارشی")) { Console.WriteLine($"Word: {item.Word}"); Console.WriteLine($"Distance: {item.Distance}"); }
top=6 در این کوئری خاص یعنی 6 رکورد را بازگشت بده.
یک نکته: اگر میخواهید کوئری فوق را توسط برنامهی «DB Browser for SQLite» اجرا کنید، باید از منوی tools آن، گزینهی load extension را انتخاب کرده و فایل dll افزونه را به برنامه معرفی کنید.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
- اصل Fork در اینجا
- تاریخچهی این Fork غیر رسمی در اینجا
- بستهی نیوگت آن در اینجا
چون تیم EF در نگارش فعلی این کتابخانه حاضر به افزودن این نوع جدید نشدهاست، بنابراین بجای بستهی اصلی Entity framework نیاز است بستهی EntityFrameworkWithHierarchyId را نصب کنید.
PM> install-package EntityFrameworkWithHierarchyId
یک تذکر مهم:
چون امضای دیجیتال این بسته، با امضای دیجیتال بستهی اصلی EF یکی نیست، اگر پروژهی شما صرفا از EF استفاده میکند، مشکلی نخواهید داشت. اما اگر برای مثال از ASP.NET Identity کامپایل شدهی برای کار با EF اصلی استفاده کنید، پیام یافت نشدن DLL مرتبط را دریافت خواهید کرد.
تعریفی مدلی با خاصیتی از نوع جدید HierarchyId
public class Employee { public int Id { get; set; } [Required, MaxLength(100)] public string Name { get; set; } [Required] public HierarchyId Node { get; set; } // نوع داده جدید }
تعریف Context و مقدار دهی اولیهی آن
در این حالت Context برنامه به همراه تنظیمات اولیهی Migrations آن یک چنین شکلی را پیدا خواهد کرد:
public class MyContext : DbContext { public DbSet<Employee> Employees { get; set; } public MyContext() : base("Connection1") { this.Database.Log = log => Console.WriteLine(log); } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { if (context.Employees.Any()) return; context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD NodePath as Node.ToString() persisted"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD Level AS Node.GetLevel() persisted"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD ManagerNode as Node.GetAncestor(1) persisted"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD ManagerNodePath as Node.GetAncestor(1).ToString() persisted"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD CONSTRAINT [UK_EmployeeNode] UNIQUE NONCLUSTERED (Node)"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] WITH CHECK ADD CONSTRAINT [EmployeeManagerNodeNodeFK] " + "FOREIGN KEY([ManagerNode]) REFERENCES [dbo].[Employees] ([Node])"); context.Employees.Add(new Employee { Name = "Root", Node = new HierarchyId("/") }); context.Employees.Add(new Employee { Name = "Emp1", Node = new HierarchyId("/1/") }); context.Employees.Add(new Employee { Name = "Emp2", Node = new HierarchyId("/2/") }); context.Employees.Add(new Employee { Name = "Emp3", Node = new HierarchyId("/1/1/") }); context.Employees.Add(new Employee { Name = "Emp4", Node = new HierarchyId("/1/1/1/") }); context.Employees.Add(new Employee { Name = "Emp5", Node = new HierarchyId("/2/1/") }); context.Employees.Add(new Employee { Name = "Emp6", Node = new HierarchyId("/1/2/") }); base.Seed(context); } }
همچنین چند فیلد محاسباتی نیز بر اساس امکانات توکار SQL Server اضافه شدهاند. متدهایی مانند ToString، GetLevel، GetAncestor و امثال آن جزئی از پیاده سازی توکار SQL Server هستند. همچنین این متدها توسط کتابخانهی EntityFrameworkWithHierarchyId نیز ارائه شدهاند.
کوئری نویسی
مرتب سازی رکوردها بر اساس HierarchyId آنها
using (var context = new MyContext()) { Console.WriteLine("\ngetItems OrderByDescending(employee => employee.Node)"); var employees = context.Employees.OrderByDescending(employee => employee.Node).ToList(); foreach (var employee in employees) { Console.WriteLine("{0} {1}", employee.Id, employee.Node); } }
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Node] AS [Node] FROM [dbo].[Employees] AS [Extent1] ORDER BY [Extent1].[Node] DESC 6 /2/1/ 3 /2/ 7 /1/2/ 5 /1/1/1/ 4 /1/1/ 2 /1/ 1 /
یافتن یک HierarchyId خاص و سپس یافتن کلیهی فرزندان آن در یک سطح پایینتر
using (var context = new MyContext()) { Console.WriteLine("\nGetAncestor(1) of /1/"); var firstItem = context.Employees.Single(employee => employee.Node == new HierarchyId("/1/")); foreach (var item in context.Employees.Where(employee => firstItem.Node == employee.Node.GetAncestor(1))) { Console.WriteLine("{0} {1}", item.Id, item.Name); } }
با این خروجی:
SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Node] AS [Node] FROM [dbo].[Employees] AS [Extent1] WHERE cast('/1/' as hierarchyid) = [Extent1].[Node] SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Node] AS [Node] FROM [dbo].[Employees] AS [Extent1] WHERE (@p__linq__0 = ([Extent1].[Node].GetAncestor(1))) OR ((@p__linq__0 IS NULL) AND ([Extent1].[Node].GetAncestor(1) IS NULL)) -- p__linq__0: '/1/' (Type = Object) 4 Emp3 7 Emp6
کوئریهای فوق را میتوان بجای استفاده از متد GetAncestor، با استفاده از متد IsDescendantOf به شکل زیر نیز نوشت:
var list = context.Employees.Where( employee => employee.Node.IsDescendantOf(new HierarchyId("/1/")) && employee.Node.GetLevel() == 2).ToList();
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Node] AS [Node] FROM [dbo].[Employees] AS [Extent1] WHERE (([Extent1].[Node].IsDescendantOf(cast('/1/' as hierarchyid))) = 1) AND (2 = ([Extent1].[Node].GetLevel()))
جابجا کردن نودها توسط متد GetReparentedValue
در کوئری ذیل، تمامی فرزندان ریشهی /1/ یافت شده و سپس والد آنها به صورت پویا تغییر داده میشود:
var items = context.Employees.Where(employee => employee.Node.IsDescendantOf(new HierarchyId("/1/"))) .Select(employee => new { Id = employee.Id, OrigPath = employee.Node, ReparentedValue = employee.Node.GetReparentedValue(new HierarchyId("/1/"), HierarchyId.GetRoot()), Level = employee.Node.GetLevel() }).ToList(); foreach (var item in items) { Console.WriteLine("Id:{0}; OrigPath:{1}; ReparentedValue:{2}; Level:{3}", item.Id, item.OrigPath, item.ReparentedValue, item.Level); }
SELECT [Extent1].[Id] AS [Id], [Extent1].[Node] AS [Node], [Extent1].[Node].GetReparentedValue(cast('/1/' as hierarchyid), hierarchyid::GetRoot()) AS [C1], [Extent1].[Node].GetLevel() AS [C2] FROM [dbo].[Employees] AS [Extent1] WHERE ([Extent1].[Node].IsDescendantOf(cast('/1/' as hierarchyid))) = 1 Id:2; OrigPath:/1/; ReparentedValue:/; Level:1 Id:4; OrigPath:/1/1/; ReparentedValue:/1/; Level:2 Id:5; OrigPath:/1/1/1/; ReparentedValue:/1/1/; Level:3 Id:7; OrigPath:/1/2/; ReparentedValue:/2/; Level:2
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
HierarcyIdTests.zip
namespace MyNewsWCFLibrary { using System; using System.Collections.Generic; public partial class tblNews { public int tblNewsId { get; set; } public int tblCategoryId { get; set; } public string Title { get; set; } public string Description { get; set; } public System.DateTime RegDate { get; set; } public Nullable<bool> IsDeleted { get; set; } public virtual tblCategory tblCategory { get; set; } } }
using System.Runtime.Serialization; namespace MyNewsWCFLibrary { using System; using System.Collections.Generic; [DataContract] public partial class tblNews { [DataMember] public int tblNewsId { get; set; } [DataMember] public int tblCategoryId { get; set; } [DataMember] public string Title { get; set; } [DataMember] public string Description { get; set; } [DataMember] public System.DateTime RegDate { get; set; } [DataMember] public Nullable<bool> IsDeleted { get; set; } public virtual tblCategory tblCategory { get; set; } } }
namespace MyNewsWCFLibrary { using System; using System.Collections.Generic; using System.Runtime.Serialization; [DataContract] public partial class tblCategory { public tblCategory() { this.tblNews = new HashSet<tblNews>(); } [DataMember] public int tblCategoryId { get; set; } [DataMember] public string CatName { get; set; } [DataMember] public bool IsDeleted { get; set; } public virtual ICollection<tblNews> tblNews { get; set; } } }
BEGIN TRANSACTION GO ALTER TABLE dbo.tblNews DROP CONSTRAINT FK_tblNews_tblCategory GO ALTER TABLE dbo.tblCategory SET (LOCK_ESCALATION = TABLE) GO COMMIT BEGIN TRANSACTION GO CREATE TABLE dbo.Tmp_tblNews ( tblNewsId int NOT NULL IDENTITY (1, 1), tblCategoryId int NOT NULL, Title nvarchar(50) NOT NULL, Description nvarchar(MAX) NOT NULL, RegDate datetime NOT NULL, IsDeleted bit NOT NULL ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO ALTER TABLE dbo.Tmp_tblNews SET (LOCK_ESCALATION = TABLE) GO ALTER TABLE dbo.Tmp_tblNews ADD CONSTRAINT DF_tblNews_IsDeleted DEFAULT 0 FOR IsDeleted GO SET IDENTITY_INSERT dbo.Tmp_tblNews ON GO IF EXISTS(SELECT * FROM dbo.tblNews) EXEC('INSERT INTO dbo.Tmp_tblNews (tblNewsId, tblCategoryId, Title, Description, RegDate, IsDeleted) SELECT tblNewsId, tblCategoryId, Title, Description, RegDate, IsDeleted FROM dbo.tblNews WITH (HOLDLOCK TABLOCKX)') GO SET IDENTITY_INSERT dbo.Tmp_tblNews OFF GO DROP TABLE dbo.tblNews GO EXECUTE sp_rename N'dbo.Tmp_tblNews', N'tblNews', 'OBJECT' GO ALTER TABLE dbo.tblNews ADD CONSTRAINT PK_tblNews PRIMARY KEY CLUSTERED ( tblNewsId ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO ALTER TABLE dbo.tblNews ADD CONSTRAINT FK_tblNews_tblCategory FOREIGN KEY ( tblCategoryId ) REFERENCES dbo.tblCategory ( tblCategoryId ) ON UPDATE NO ACTION ON DELETE NO ACTION GO COMMIT
Database.SetInitializer(new MigrateDatabaseToLatestVersion<ApplicationDbContext, Configuration>()); SmObjectFactory.Container.GetInstance<IUnitOfWork>().ForceDatabaseInitialize();
ساختار پوشهها و پروژههای قسمت Blazor Server
قسمت Blazor Server مدیریت هتل ما از 7 پروژه و پوشهی زیر تشکیل میشود:
- BlazorServer.App: پروژهی اصلی Blazor Server است که با اجرای دستور dotnet new blazorserver در پوشهی خالی آن آغاز میشود.
- BlazorServer.Common: پروژهای از نوع classlib، جهت قرارگیری کدهای مشترک بین پروژهها است که با اجرای دستور dotnet new classlib در این پوشه آغاز میشود.
- BlazorServer.DataAccess: پروژهای از نوع classlib، برای تعریف DbContext برنامه است که با اجرای دستور dotnet new classlib در این پوشه آغاز میشود.
- BlazorServer.Entities: پروژهای از نوع classlib، جهت تعریف کلاسهای متناظر با جداول بانک اطلاعاتی برنامه است که با اجرای دستور dotnet new classlib در این پوشه آغاز میشود.
- BlazorServer.Models: پروژهای از نوع classlib، برای تعریف کلاسهای data transfer objects برنامه (DTO's) است که با اجرای دستور dotnet new classlib در این پوشه آغاز میشود.
- BlazorServer.Models.Mappings: پروژهای از نوع classlib، برای تعریف نگاشتهای بین DTO's و مجودیتهای برنامه و برعکس است که با اجرای دستور dotnet new classlib در این پوشه آغاز میشود.
- BlazorServer.Services: پروژهای از نوع classlib، جهت تعریف کدهایی که منطق تجاری تعامل با بانک اطلاعاتی را از طریق BlazorServer.DataAccess میسر میکند که با اجرای دستور dotnet new classlib در این پوشه آغاز میشود.
اصلاح پروژهی BlazorServer.App جهت استفاده از LibMan
قالب پیشفرض BlazorServer.App، به همراه پوشهی wwwroot\css است که در آن بوت استرپ و open-iconic به همراه فایل site.css قرار دارند. چون این پروژه به همراه هیچ نوع روشی برای مدیریت نگهداری بستههای سمت کلاینت خود نیست، دو پوشهی بوت استرپ و open-iconic آنرا حذف کرده و از روش مطرح شدهی در مطلب «Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت» استفاده خواهیم کرد:
dotnet tool update -g Microsoft.Web.LibraryManager.Cli libman init libman install bootstrap --provider unpkg --destination wwwroot/lib/bootstrap libman install open-iconic --provider unpkg --destination wwwroot/lib/open-iconic
بعد از نصب بستههای ذکر شده، ابتدا سطر زیر را از ابتدای فایل پیشفرض wwwroot\css\site.css حذف میکنیم:
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>BlazorServer.App</title> <base href="~/" /> <link href="lib/open-iconic/font/css/open-iconic-bootstrap.min.css" rel="stylesheet" /> <link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" /> <link href="css/site.css" rel="stylesheet" /> <link href="BlazorServer.App.styles.css" rel="stylesheet" /> </head>
برای bundling & minification این فایلها میتوان از «Bundler Minifier» استفاده کرد.
پروژهی موجودیتهای مدیریت هتل
فایل BlazorServer.Entities.csproj وابستگی خاصی را نداشته و به صورت زیر تعریف شدهاست:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> </Project>
using System; using System.ComponentModel.DataAnnotations; namespace BlazorServer.Entities { public class HotelRoom { [Key] public int Id { get; set; } [Required] public string Name { get; set; } [Required] public int Occupancy { get; set; } [Required] public decimal RegularRate { get; set; } public string Details { get; set; } public string SqFt { get; set; } public string CreatedBy { get; set; } public DateTime CreatedDate { get; set; } = DateTime.Now; public string UpdatedBy { get; set; } public DateTime UpdatedDate { get; set; } } }
پروژهی تعریف DbContext برنامهی مدیریت هتل
فایل BlazorServer.DataAccess.csproj به این صورت تعریف شدهاست:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\BlazorServer.Entities\BlazorServer.Entities.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.3"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> </Project>
- همچنین دو وابستگی مورد نیاز جهت کار با EntityFrameworkCore و اجرای مهاجرتها را نیز به آن افزودهایم.
پس از تامین این وابستگیها، اکنون میتوان DbContext ابتدایی برنامه را به صورت زیر تعریف کرد که کار آن، در معرض دید قرار دادن HotelRoom به صورت یک DbSet است:
using BlazorServer.Entities; using Microsoft.EntityFrameworkCore; namespace BlazorServer.DataAccess { public class ApplicationDbContext : DbContext { public DbSet<HotelRoom> HotelRooms { get; set; } public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } } }
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\BlazorServer.DataAccess\BlazorServer.DataAccess.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> </Project>
- سپس چون میخواهیم از تامین کنندهی بانک اطلاعاتی SQL Server نیز استفاده کنیم، وابستگیهای آنرا نیز افزودهایم.
با این تنظیمات، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و کار افزودن AddDbContext و UseSqlServer را انجام میدهیم تا DbContext برنامه از طریق تزریق وابستگیها قابل دسترسی شود و همچنین رشتهی اتصالی مشخص شده نیز به تامین کنندهی SQL Server ارسال شود:
namespace BlazorServer.App { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { var connectionString = Configuration.GetConnectionString("DefaultConnection"); services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); // ...
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=HotelManagement;Trusted_Connection=True;MultipleActiveResultSets=true" } }
اجرای مهاجرتها و تشکیل ساختار بانک اطلاعاتی
پس از این تنظیمات، اکنون میتوانیم به پوشهی BlazorServer\BlazorServer.DataAccess مراجعه کرده و از طریق خط فرمان، دستورات زیر را صادر کنیم:
dotnet tool update --global dotnet-ef --version 5.0.3 dotnet build dotnet ef migrations --startup-project ../BlazorServer.App/ add Init --context ApplicationDbContext dotnet ef --startup-project ../BlazorServer.App/ database update --context ApplicationDbContext
- همیشه بهتر است پیش از اجرای عملیات Migration، یکبار dotnet build را اجرا کرد؛ تا اگر خطایی وجود دارد، بتوان جزئیات دقیق آنرا مشاهده کرد. چون عموما این جزئیات در حین اجرای دستورات بعدی، با پیام مختصر «عملیات شکست خورد»، نمایش داده نمیشوند.
- دستور سوم، کار تشکیل پوشهی BlazorServer\BlazorServer.DataAccess\Migrations و تولید خودکار دستورات تشکیل بانک اطلاعاتی را بر اساس ساختار DbContext برنامه انجام میدهد.
- دستور چهارم، بر اساس اطلاعات موجود در پوشهی BlazorServer\BlazorServer.DataAccess\Migrations، بانک اطلاعاتی واقعی را تولید میکند.
در این دستورات ذکر پروژهی آغازین برنامه جهت یافتن وابستگیهای پروژه ضروری است.
تکمیل پروژهی DTOهای برنامه
همواره توصیه شدهاست که موجودیتهای برنامه را مستقیما در معرض دید UI قرار ندهید. حداقل مشکلی را که در اینجا ممکن است مشاهده کنید، حملات از نوع mass assignment هستند. برای مثال قرار است از کاربر، کلمهی عبور جدید آنرا دریافت کنید، ولی چون اطلاعات دریافتی، به اصل موجودیت متناظر با بانک اطلاعاتی نگاشت میشود، کاربر میتواند فیلد IsAdmin را هم خودش مقدار دهی کند! و چون سیستم Binding بسیار پیشرفته عمل میکند، این ورودی را معتبر یافته و در اینجا علاوه بر به روز رسانی کلمهی عبور، خواص دیگری را هم که نباید به روز رسانی شوند، به روز رسانی میکند و یا در بسیاری از موارد نیاز است data annotations خاصی را برای فیلدها تعریف کرد که ربطی به موجودیت اصلی ندارند و یا نیاز است فیلدهایی را در UI قرار داد که باز هم تناظر یک به یکی با موجودیت اصلی ندارند (گاهی کمتر و گاهی بیشتر هستند و باید بر روی آنها محاسباتی صورت گیرد تا قابلیت ذخیره سازی در بانک اطلاعاتی را پیدا کنند). به همین جهت کار مدل سازی UI و یا بازگشت اطلاعات نهایی از سرویسها را توسط DTOها که یک سری کلاس سادهی C# 9.0 از نوع record هستند، انجام میدهیم:
using System; using System.ComponentModel.DataAnnotations; namespace BlazorServer.Models { public record HotelRoomDTO { public int Id { get; init; } [Required(ErrorMessage = "Please enter the room's name")] public string Name { get; init; } [Required(ErrorMessage = "Please enter the occupancy")] public int Occupancy { get; init; } [Range(1, 3000, ErrorMessage = "Regular rate must be between 1 and 3000")] public decimal RegularRate { get; init; } public string Details { get; init; } public string SqFt { get; init; } } }
در اینجا فیلدهای UI برنامه را که در قسمت بعد تکمیل خواهیم کرد، مشاهده میکنید؛ به همراه یک سری data annotation برای تعریف اجباری و یا بازهی مورد قبول، به همراه پیامهای خطای مرتبط.
نگاشت DTOهای برنامه به موجودیتها و بر عکس
یا میتوان خواص DTO تعریف شده را یکی یکی به موجودیتی متناظر با آن انتساب داد و یا میتوان از AutoMapper برای اینکار استفاده کرد. به همین جهت به BlazorServer.Models.Mappings.csproj مراجعه کرده و تغییرات زیر را اعمال میکنیم:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\BlazorServer.Entities\BlazorServer.Entities.csproj" /> <ProjectReference Include="..\BlazorServer.Models\BlazorServer.Models.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" /> </ItemGroup> </Project>
- همچنین بستهی مخصوص AutoMapper را که به همراه امکانات تزریق وابستگیهای آن نیز هست، در اینجا افزودهایم.
پس از افزودن این ارجاعات، نگاشت دو طرفهی بین مدل و موجودیت تعریف شده را به صورت زیر تعریف میکنیم:
using AutoMapper; using BlazorServer.Entities; namespace BlazorServer.Models.Mappings { public class MappingProfile : Profile { public MappingProfile() { CreateMap<HotelRoomDTO, HotelRoom>().ReverseMap(); // two-way mapping } } }
namespace BlazorServer.App { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); // ...
تعریف سرویس مدیریت اتاقهای هتل
پس از راه اندازی برنامه و تعریف موجودیتها، DbContext و غیره، اکنون میتوانیم از آنها جهت ارائهی منطق مدیریتی برنامه استفاده کنیم:
using System.Collections.Generic; using System.Threading.Tasks; using BlazorServer.Models; namespace BlazorServer.Services { public interface IHotelRoomService { Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO); Task<int> DeleteHotelRoomAsync(int roomId); IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync(); Task<HotelRoomDTO> GetHotelRoomAsync(int roomId); Task<HotelRoomDTO> IsRoomUniqueAsync(string name); Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO); } }
که پیاده سازی ابتدایی آن به صورت زیر است:
using System; using System.Collections.Generic; using System.Threading.Tasks; using AutoMapper; using AutoMapper.QueryableExtensions; using BlazorServer.DataAccess; using BlazorServer.Entities; using BlazorServer.Models; using Microsoft.EntityFrameworkCore; namespace BlazorServer.Services { public class HotelRoomService : IHotelRoomService { private readonly ApplicationDbContext _dbContext; private readonly IMapper _mapper; private readonly IConfigurationProvider _mapperConfiguration; public HotelRoomService(ApplicationDbContext dbContext, IMapper mapper) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _mapperConfiguration = mapper.ConfigurationProvider; } public async Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO) { var hotelRoom = _mapper.Map<HotelRoom>(hotelRoomDTO); hotelRoom.CreatedDate = DateTime.Now; hotelRoom.CreatedBy = ""; var addedHotelRoom = await _dbContext.HotelRooms.AddAsync(hotelRoom); await _dbContext.SaveChangesAsync(); return _mapper.Map<HotelRoomDTO>(addedHotelRoom.Entity); } public async Task<int> DeleteHotelRoomAsync(int roomId) { var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId); if (roomDetails == null) { return 0; } _dbContext.HotelRooms.Remove(roomDetails); return await _dbContext.SaveChangesAsync(); } public IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync() { return _dbContext.HotelRooms .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .AsAsyncEnumerable(); } public Task<HotelRoomDTO> GetHotelRoomAsync(int roomId) { return _dbContext.HotelRooms .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .FirstOrDefaultAsync(x => x.Id == roomId); } public Task<HotelRoomDTO> IsRoomUniqueAsync(string name) { return _dbContext.HotelRooms .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .FirstOrDefaultAsync(x => x.Name == name); } public async Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO) { if (roomId != hotelRoomDTO.Id) { return null; } var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId); var room = _mapper.Map(hotelRoomDTO, roomDetails); room.UpdatedBy = ""; room.UpdatedDate = DateTime.Now; var updatedRoom = _dbContext.HotelRooms.Update(room); await _dbContext.SaveChangesAsync(); return _mapper.Map<HotelRoomDTO>(updatedRoom.Entity); } } }
- در این کدها استفاده از متد ProjectTo را هم مشاهده میکنید. استفاده از این متد، بسیار بهینهتر از کار با متد Map درون حافظهای است. از این جهت که بر روی SQL نهایی ارسالی به سمت سرور تاثیرگذار است و تعداد فیلدهای بازگشت داده شده را بر اساس DTO تعیین شده، کاهش میدهد. درغیراینصورت باید تمام ستونهای جدول را بازگشت داد و سپس با استفاده از متد Map درون حافظهای، کار نگاشت نهایی را انجام داد که آنچنان بهینه نیست.
در آخر نیاز است این سرویس را نیز به سیستم تزریق وابستگیهای برنامه معرفی کنیم. به همین جهت در فایل BlazorServer\BlazorServer.App\Startup.cs، تغییر زیر را اعمال خواهیم کرد:
namespace BlazorServer.App { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddScoped<IHotelRoomService, HotelRoomService>(); // ...
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-13.zip
EF Core چیست؟
EF Core یک ORM یا object-relational mapper چندسکویی است که امکان کار با بانکهای اطلاعاتی مختلف را از طریق اشیاء دات نتی میسر میکند. توسط آن قسمت عمدهی کدهای مستقیم کار با بانکهای اطلاعاتی حذف شده و تبدیل به کدهای دات نتی میشوند. مزیت این لایهی Abstraction اضافی (لایهای بر روی کدهای مستقیم لایه ADO.NET زیرین)، امکان تعویض بانک اطلاعاتی مورد استفاده، تنها با تغییر کدهای آغازین برنامهاست؛ بدون نیاز به تغییری در سایر قسمتهای برنامه. همچنین کار با اشیاء دات نتی و LINQ، مزایایی مانند تحت نظر قرار گرفتن کدها توسط کامپایلر و برخورداری از ابزارهای Refactoring پیشرفته را میسر میکنند. به علاوه SQL خودکار تولیدی توسط آن نیز همواره پارامتری بوده و مشکلات حملات تزریق SQL در این حالت تقریبا به صفر میرسند (اگر مستقیما SQL نویسی نکنید و صرفا از LINQ استفاده کنید). مزیت دیگر همواره پارامتری بودن کوئریها، رفتار بسیاری از بانکهای اطلاعاتی با آنها همانند رویههای ذخیره شده است که به عدم تولید Query planهای مجزایی به ازای هر کوئری رسیده منجر میشود که در نهایت سبب بالا رفتن سرعت اجرای کوئریها و مصرف حافظهی کمتری در سمت سرور بانک اطلاعاتی میگردد.
تفاوت EF Core با نگارشهای دیگر Entity framework در چیست؟
سورس باز بودن
EF از نگارشهای آخر آن بود که سورس باز شد؛ اما EF Core از زمان نگارشهای پیش نمایش آن به صورت سورس باز در GitHub قابل دسترسی است.
چند سکویی بودن
EF Core برخلاف EF 6.x (آخرین نگارش مبتنی بر Full Framework آن)، نه تنها چندسکویی است و قابلیت اجرای بر روی Mac و لینوکس را نیز دارا است، به علاوه امکان استفادهی از آن در انواع و اقسام برنامههای دات نتی مانند UWP یا Universal Windows Platform و Windows phone که پیشتر با EF 6.x میسر نبود، وجود دارد. لیست این نوع سکوها و برنامههای مختلف به شرح زیر است:
• All .NET application (Console, ASP.NET 4, WinForms, WPF) • Mac and Linux applications (Mono) • UWP (Universal Windows Platform) • ASP.NET Core applications • Can use EF Core in Windows phone and Windows store app
افزایش تعداد بانکهای اطلاعاتی پشتیبانی شده
در EF Full یا EF 6.x، هدف اصلی، تنها کار با بانکهای اطلاعاتی رابطهای بود و همچنین مایکروسافت صرفا نگارشهای مختلف SQL Server را به صورت رسمی پشتیبانی میکرد و برای سایر بانکهای اطلاعاتی دیگر باید از پروایدرهای ثالث استفاده کرد.
در EF Core علاوه بر افزایش تعداد پروایدرهای رسمی بانکهای اطلاعاتی رابطهای، پشتیبانی از بانکهای اطلاعاتی NoSQL هم اضافه شدهاست؛ به همراه پروایدر ویژهای به نام In Memory جهت انجام سادهتر Unit testing. کاری که با نگارشهای پیشین EF به سادگی و از روز اول پشتیبانی نمیشد.
حذف و یا عدم پیاده سازی تعدادی از قابلیتهای EF 6.x
اگر موارد فوق جزو مهمترین مزایای کار با EF Core باشند، باید درنظر داشت که به علت حذف و یا تقلیل یافتن یک سری از ویژگیها در NET Core.، مانند Reflection (جهت پشتیبانی از دات نت در سکوهای مختلف کاری و خصوصا پشتیبانی از حالتی که کامپایلر مخصوص برنامههای UWP نیاز دارد تمام نوعها را همانند زبانهای C و ++C، در زمان کامپایل بداند)، یک سری از قابلیتهای EF 6.x مانند Lazy loading هنوز در EF Core پشتیبانی نمیشوند. لیست کامل و به روز شدهی این موارد را در اینجا میتوانید مطالعه کنید.
بنابراین امکان انتقال برنامههای EF 6.x به EF Core 1.0 عموما وجود نداشته و نیاز به بازنویسی کامل دارند. هرچند بسیاری از مفاهیم آن با EF Code First یکی است.
برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0
برای نصب تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0، جهت کار با مشتقات SQL Server (و SQL LocalDB) نیاز است سه بستهی ذیل را نصب کرد (از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console):
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design
پس از اجرای سه دستور فوق، تغییرات مداخل فایل project.json برنامه به صورت ذیل خواهند بود:
{ "dependencies": { // same as before "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final", "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0" } }
{ "dependencies": { // same as before "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "type": "build" }, "Microsoft.EntityFrameworkCore.SqlServer.Design": { "version": "1.0.0", "type": "build" } }, "tools": { // same as before "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "imports": [ "portable-net45+win8" ] } } }
بنابراین از همین ابتدای کار، بدون مراجعهی به Package Manager Console، چهار تغییر فوق را به فایل project.json اعمال کرده و آنرا ذخیره کنید؛ تا کار به روز رسانی و نصب بستهها، به صورت خودکار و همچنین صحیحی انجام شود.
فعال سازی صفحات مخصوص توسعه دهندههای EF Core 1.0
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهندهها» با تعدادی از اینگونه صفحات آشنا شدیم. برای EF Core نیز بستهی مخصوصی به نام Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore وجود دارد که امکان فعال سازی صفحهی نمایش خطاهای بانک اطلاعاتی را میسر میکند. بنابراین ابتدا به فایل project.json مراجعه کرده و این بسته را اضافه کنید:
{ "dependencies": { // same as before "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0" } }
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDatabaseErrorPage(); }
تعریف اولین Context برنامه و مشخص سازی رشتهی اتصالی آن
در این تصویر، زیر ساخت نگاشتهای EF Core را مشاهده میکنید. در سمت چپ، ظرفی را داریم به نام DB Context که در برگیرندهی Db Setها است. در سمت راست که بیانگر ساختار کلی یک بانک اطلاعاتی است، معادل اینها را مشاهده میکنیم. هر Db Set به یک جدول بانک اطلاعاتی نگاشت خواهد شد و متشکل است از کلاسی به همراه یک سری خواص که اینها نیز به فیلدها و ستونهای آن جدول در سمت بانک اطلاعاتی نگاشت میشوند.
بنابراین برای شروع کار، پوشهای را به نام Entities به پروژه اضافه کرده و سپس کلاس ذیل را به آن اضافه میکنیم:
namespace Core1RtmEmptyTest.Entities { public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } }
using Microsoft.EntityFrameworkCore; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Person> Persons { get; set; } } }
سازندهی این کلاس نیز به نحو خاصی تعریف شدهاست. اگر به سورسهای EF Core مراجعه کنیم، کلاس پایهی DbContext دارای دو سازندهی با و بدون پارامتر است:
protected DbContext() : this((DbContextOptions) new DbContextOptions<DbContext>()) { } public DbContext([NotNull] DbContextOptions options) { // … }
using Microsoft.EntityFrameworkCore; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"... connection string ..."); } } }
اما چون در یک برنامهی ASP.NET Core، کار ثبت سرویس مربوط به EF Core، در کلاس آغازین برنامه انجام میشود و در آنجا به سادگی میتوان به خاصیت Configuration برنامه دسترسی یافت و توسط آن رشتهی اتصالی را دریافت کرد، مرسوم است از سازندهی با پارامتر DbContext به نحوی که در ابتدا عنوان شد، استفاده شود.
بنابراین در ادامه، پس از مطالعهی مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» به فایل appsettings.json مراجعه کرده و تنظیمات رشتهی اتصالی برنامه را به صورت ذیل در آن مشخص میکنیم:
{ "ConnectionStrings": { "ApplicationDbContextConnection": "Data Source=(local);Initial Catalog=TestDbCore2016;Integrated Security = true" } }
در اینجا به وهلهی پیش فرض SQL Server اشاره شدهاست؛ از حالت اعتبارسنجی ویندوزی SQL Server استفاده میشود و بانک اطلاعاتی جدیدی به نام TestDbCore2016 در آن مشخص گردیدهاست.
پس از تعریف رشتهی اتصالی، متد OnConfiguring را از کلاس ApplicationDbContext حذف کرده و از همان نگارش دارای سازندهی با پارامتر آن استفاده میکنیم. برای اینکار به کلاس آغازین برنامه مراجعه کرده و توسط متد AddDbContext این Context را به سرویسهای ASP.NET Core معرفی میکنیم:
public class Startup { public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false) .AddJsonFile($"appsettings.{env}.json", optional: true); Configuration = builder.Build(); } public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; }); services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(Configuration["ConnectionStrings:ApplicationDbContextConnection"]); });
بنابراین قسمت options.UseSqlServer را یا در اینجا مقدار دهی میکنید و یا از طریق بازنویسی متد OnConfiguring کلاس Context برنامه.
یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه وجود دارد
با توجه به اینکه Context برنامه را به صورت یک سرویس به ASP.NET Core معرفی کردیم، امکان تزریق وابستگیها نیز در آن وجود دارد. یعنی بجای روش فوق، میتوان IConfigurationRoot را به سازندهی کلاس Context برنامه نیز تزریق کرد:
using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { private readonly IConfigurationRoot _configuration; public ApplicationDbContext(IConfigurationRoot configuration) { _configuration = configuration; } //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) //{ //} public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(_configuration["ConnectionStrings:ApplicationDbContextConnection"]); } } }
در این حالت متد ConfigureServices کلاس آغازین برنامه، چنین شکلی را پیدا میکند و ساده میشود:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>();
یک نکته: امکان تزریق ApplicationDbContext به تمام کلاسهای برنامه وجود دارد
همینقدر که ApplicationDbContext را به عنوان سرویسی در ConfigureServices تعریف کردیم، امکان تزریق آن در اجزای مختلف یک برنامهی ASP.NET Core نیز وجود دارد:
using System.Linq; using Core1RtmEmptyTest.Entities; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class TestDBController : Controller { private readonly ApplicationDbContext _ctx; public TestDBController(ApplicationDbContext ctx) { _ctx = ctx; } public IActionResult Index() { var name = _ctx.Persons.First().FirstName; return Json(new { firstName = name }); } } }
در این حالت پس از اجرای برنامه، خطای ذیل را مشاهده خواهیم کرد:
علت اینجا است که هنوز این بانک اطلاعاتی ایجاد نشدهاست و همچنین ساختار جداول را به آن منتقل نکردهایم که این موارد را در قسمتهای بعدی مرور خواهیم کرد.
node -v npm -v
فایل پیشین angular-cli.json حذف و فایل جدید angular.json بجای آن معرفی شدهاست
یکی از مهمترین تغییرات CLI 6.0 نسبت به نگارشهای قبلی آن، پشتیبانی از چندین پروژه است و به همین منظور ساختار فایل تنظیمات آنرا به طور کامل تغییر دادهاند و اگر دستور ng new project1 را صادر کنید، دیگر از فایل پیشین angular-cli.json خبری نبوده و بجای آن فایل جدید angular.json قابل مشاهدهاست:
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "projects": { "project1": {
مزیت مهم این قابلیت، امکان ایجاد libraries است که به صورت توکار از این نگارش پشتیبانی میشود و میتوان اجزایی مانند components ،directives ،pipes و services اشتراکی را در یک یا چندین کتابخانه قرار داد و سپس از آنها در پروژهی اصلی و یا چندین پروژهی متصل استفاده کرد.
نگارش 6 در پشت صحنه، پروژهی موفق ng-packagr را به مجموعهی CLI اضافه کردهاست و از این پس توسط خود CLI میتوان کتابخانههای استاندارد Angular را تولید کرد. این مورد، مزیت استاندارد سازی کتابخانههای npm حاصل را نیز به همراه دارد؛ مشکلی که گاهی از اوقات به علت عدم رعایت این ساختار، با بستههای فعلی npm مخصوص Angular وجود دارند. برای مثال بدون استفادهی از این ابزار، نیاز است مستندات 13 صفحهای ساخت کتابخانههای Angular را سطر به سطر پیاده سازی کنید که توسط CLI 6.0، به صورت خودکار ایجاد و مدیریت میشود.
بنابراین اکنون سؤال مهمی که مطرح میشود این است: آیا باید فایل angular-cli.json پیشین را به صورت دستی به این فایل جدید به روز رسانی کرد و چگونه؟
به روز رسانی تمام بستههای سراسری سیستم
در ادامه پیش از هر کاری نیاز است تمام بستههای سراسری npm ایی را که هر از چندگاهی به سیستم خود اضافه کردهاید، به روز رسانی کنید. برای مشاهدهی لیست موارد تاریخ مصرف گذشتهی آنها، دستور زیر را صادر کنید:
npm outdated -g --depth=0
npm update -g
به روز رسانی خودکار ساختار فایل angular-cli.json
این به روز رسانی توسط CLI 6.0 به صورت خودکار پشتیبانی میشود و شامل این مراحل است:
ابتدا نیاز است بستهی سراسری آنرا به روز رسانی کرد:
npm i -g @angular/cli
npm install --save-dev @angular/cli@latest
ng update @angular/cli ng update @angular/core ng update rxjs
DELETE .angular-cli.json CREATE angular.json (4273 bytes) UPDATE karma.conf.js (1008 bytes) UPDATE src/tsconfig.spec.json (322 bytes) UPDATE package.json (2076 bytes) UPDATE tslint.json (3217 bytes)
لیست سایر بستههایی را که میتوان توسط این دستور به روز رسانی کرد، با اجرا دستور ng update میتوانید مشاهده کنید. برای مثال اگر از Angular Material نیز استفاده میکنید، دستور به روز رسانی آن به صورت زیر است:
ng update @angular/material
مشکل! دستور ng update کار نمیکند!
اگر پروژهی شما صرفا مبتنی بر بستههای اصلی Angular باشد، مراحل یاد شدهی فوق را با موفقیت به پایان خواهید رساند. اما اگر از کتابخانههای ثالثی استفاده کرده باشید، منهای دستور «ng update @angular/cli» که کار تولید فایل جدید angular.json را انجام میدهد، مابقی با خطاهایی مانند «incompatible peer dependency» و یا «Invalid range:>=2.3.1 <3.0.0||>=4.0.0» متوقف میشوند.
در یک چنین حالتی نیاز است ابتدا وابستگیهای محلی پروژه را به روز کرد و سپس دستورات ng update را تکرار نمود. برای این منظور ابتدا بستهی npm-check-updates را نصب کنید:
npm install npm-check-updates -g
ncu -u npm install
اکنون تمام وابستگیهای محلی پروژهی شما به صورت خودکار به آخرین نگارش آنها به روز رسانی شدهاند و اینبار اگر دستورات ذیل را اجرا کنید، با خطاهای یاد شده مواجه نخواهید شد:
ng update @angular/core ng update rxjs
البته اگر در این حالت برنامه را کامپایل کنید، کار نخواهد کرد. علت اصلی آن به بهروز رسانی rxjs به نگارش 6 آن مرتبط میشود که در مطلب بعدی پیگیری خواهد شد و این نگارش شامل حذفیات بسیاری است در جهت کاهش حجم آن، یکپارچکی و یک دست شدن syntax آن و همچنین بهبود قابل ملاحظهی کارآیی آن. البته پیشنیاز الزامی آن آشنایی با pipe-able operators است. علت دیگر کامپایل نشدن برنامه هم میتواند عدم استفاده از HttpClient نگارش 4.3 به بعد باشد.
خاموش کردن اخطار «TypeScript version mismatch»
اگر نگارش TypeScript نصب شدهی در سیستم به صورت سراسری، با نگارش محلی پروژهی شما یکی نباشد، اخطار «TypeScript version mismatch» را دریافت میکنید. روش خاموش کردن آن در CLI جدید با اجرای دستور زیر است:
ng config cli.warnings.typescriptMismatch false
{ "devDependencies": { "typescript": "~2.7.2" } }
تغییرات ng build در نگارش 6
در نگارش 6، مفهوم پیشین environments به configurations تغییر یافتهاست و اینبار در فایل جدید angular.json تنظیم میشوند:
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "projects": { "angular-template-driven-forms-lab": { "configurations": { "production": { "optimization": true, "outputHashing": "all", "sourceMap": false, "extractCss": true, "namedChunks": false, "aot": true, "extractLicenses": true, "vendorChunk": false, "buildOptimizer": true, "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ] } } },
ng build --env staging
ng build --configuration staging
تغییرات نامهای نهایی تولیدی
در CLI 6.0، نامهای نهایی تولیدی دیگر به همراه bundle و یا chunk نیستند. برای مثال دستور ng build یک چنین خروجی را تولید میکند:
>ng build --watch Date: 2018-05-05T09:10:50.158Z Hash: a43eab94ff01539b8592 Time: 31733ms chunk {main} main.js, main.js.map (main) 9.38 kB [initial] [rendered] chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 226 kB [initial] [rendered] chunk {runtime} runtime.js, runtime.js.map (runtime) 5.4 kB [entry] [rendered] chunk {styles} styles.js, styles.js.map (styles) 15.6 kB [initial] [rendered] chunk {vendor} vendor.js, vendor.js.map (vendor) 3.05 MB [initial] [rendered]
و هشهای حالت prod به صورت زیر تولید و به نام فایل اضافه میشوند:
>ng build --prod --watch Date: 2018-05-05T09:17:01.803Z Hash: f25fd6788a4969c52b70 Time: 73279ms chunk {0} runtime.6afe30102d8fe7337431.js (runtime) 1.05 kB [entry] [rendered] chunk {1} styles.34c57ab7888ec1573f9c.css (styles) 0 bytes [initial] [rendered] chunk {2} polyfills.6c08419970f9e4781b69.js (polyfills) 59.4 kB [initial] [rendered]
ساده شدن افزودن وابستگیهای ثالث به پروژههای CLI
برای نصب یک کتابخانهی ثالث، پیشتر میبایستی ابتدا بستهی npm آن جداگانه نصب و سپس فایل config برنامه، جهت معرفی مداخل آن، ویرایش میشد. اکنون دستور جدید ng add تمام این مراحل را به صورت خودکار انجام میدهد:
ng add @angular/material