در این پست به شرح کلاس Rectangle جهت رسم مستطیل و Square جهت رسم مربع میپردازیم
using System.Drawing; namespace PWS.ObjectOrientedPaint.Models { /// <summary> /// Rectangle /// </summary> public class Rectangle : Shape { #region Constructors (2) /// <summary> /// Initializes a new instance of the <see cref="Rectangle" /> class. /// </summary> /// <param name="startPoint">The start point.</param> /// <param name="endPoint">The end point.</param> /// <param name="zIndex">Index of the z.</param> /// <param name="foreColor">Color of the fore.</param> /// <param name="thickness">The thickness.</param> /// <param name="isFill">if set to <c>true</c> [is fill].</param> /// <param name="backgroundColor">Color of the background.</param> public Rectangle(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor) : base(startPoint, endPoint, zIndex, foreColor, thickness, isFill, backgroundColor) { ShapeType = ShapeType.Rectangle; } /// <summary> /// Initializes a new instance of the <see cref="Rectangle" /> class. /// </summary> public Rectangle() { ShapeType = ShapeType.Rectangle; } #endregion Constructors #region Methods (1) // Public Methods (1) /// <summary> /// Draws the specified g. /// </summary> /// <param name="g">The g.</param> public override void Draw(Graphics g) { if (IsFill) g.FillRectangle(BackgroundBrush, StartPoint.X, StartPoint.Y, Width, Height); g.DrawRectangle(Pen, StartPoint.X, StartPoint.Y, Width, Height); base.Draw(g); } #endregion Methods } }
کلاس بعدی کلاس Square میباشد، که از کلاس بالا (Rectangle) ارث بری داشته است، کدهای این کلاس را در زیر مشاهده میکنید.
using System; using System.Drawing; namespace PWS.ObjectOrientedPaint.Models { /// <summary> /// Square /// </summary> public class Square : Rectangle { #region Constructors (2) /// <summary> /// Initializes a new instance of the <see cref="Square" /> class. /// </summary> /// <param name="startPoint">The start point.</param> /// <param name="endPoint">The end point.</param> /// <param name="zIndex">Index of the z.</param> /// <param name="foreColor">Color of the fore.</param> /// <param name="thickness">The thickness.</param> /// <param name="isFill">if set to <c>true</c> [is fill].</param> /// <param name="backgroundColor">Color of the background.</param> public Square(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor) { float x = 0, y = 0; float width = Math.Abs(endPoint.X - startPoint.X); float height = Math.Abs(endPoint.Y - startPoint.Y); if (startPoint.X <= endPoint.X && startPoint.Y <= endPoint.Y) { x = startPoint.X; y = startPoint.Y; } else if (startPoint.X >= endPoint.X && startPoint.Y >= endPoint.Y) { x = endPoint.X; y = endPoint.Y; } else if (startPoint.X >= endPoint.X && startPoint.Y <= endPoint.Y) { x = endPoint.X; y = startPoint.Y; } else if (startPoint.X <= endPoint.X && startPoint.Y >= endPoint.Y) { x = startPoint.X; y = endPoint.Y; } StartPoint = new PointF(x, y); var side = Math.Max(width, height); EndPoint = new PointF(x+side, y+side); ShapeType = ShapeType.Square; Zindex = zIndex; ForeColor = foreColor; Thickness = thickness; BackgroundColor = backgroundColor; IsFill = isFill; } /// <summary> /// Initializes a new instance of the <see cref="Square" /> class. /// </summary> public Square() { ShapeType = ShapeType.Square; } #endregion Constructors } }
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1#
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 2#
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 3#
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 4#
استفاده از XQuery - قسمت دوم
کوئری گرفتن از اسناد XML دارای فضای نام، توسط XQuery
در مثال زیر، تمام المانهای سند XML، در فضای نام http://www.people.com تعریف شدهاند.
DECLARE @doc XML SET @doc =' <p:people xmlns:p="http://www.people.com"> <p:person name="Vahid" /> <p:person name="Farid" /> </p:people> ' SELECT @doc.query('/people/person')
سعی دوم احتمالا روش ذیل خواهد بود
SELECT @doc.query('/p:people/p:person')
XQuery [query()]: The name "p" does not denote a namespace.
SELECT @doc.query(' declare default element namespace "http://www.people.com"; /people/person ')
SELECT @doc.query(' declare namespace aa="http://www.people.com"; /aa:people/aa:person ')
روش دیگر تعریف فضای نام، استفاده از WITH XMLNAMESPACES، پیش از تعریف کوئری است:
WITH XMLNAMESPACES(DEFAULT 'http://www.people.com') SELECT @doc.query('/people/person')
در اینجا نیز امکان کار با چندین فضای نام وجود دارد و برای این منظور تنها کافی است از تعریف Alias استفاده شود. فضاهای نام بعدی با یک کاما از هم مجزا خواهند شد.
WITH XMLNAMESPACES('http://www.people.com' AS aa) SELECT @doc.query('/aa:people/aa:person')
عبارات XPath و FLOWR
XQuery از دو نوع عبارت XPath و FLOWR میتواند استفاده کند. XQuery همیشه از XPath برای انتخاب دادهها و نودها استفاده میکند. در اینجا هر نوع XPath سازگار با استاندارد 2 آن، یک XQuery نیز خواهد بود. برای انجام اعمالی بجز انتخاب دادهها، باید از عبارات FLOWR استفاده کرد؛ برای مثال برای ایجاد حلقه، مرتب سازی و یا ایجاد نودهای جدید.
در مثال زیر که data آن در قسمت قبل تعریف شد، دو کوئری نوشته شده یکی هستند:
SELECT @data.query(' (: FLOWE :) for $p in /people/person where $p/age > 30 return $p ') SELECT @data.query(' (: XPath :) /people/person[age>30] ')
XPath بسیار شبیه به مسیر دهیهای یونیکسی است. بسیار فشرده بوده و همچنین مناسب است برای کار با ساختارهای تو در تو و سلسله مراتبی. مثال زیر را درنظر بگیرید:
/books/book[1]/title/chapter
در XPath توسط قابلیتی به نام محور میتوان به المانهای قبلی یا بعدی دسترسی پیدا کرد. این محورهای پشتیبانی شده در SQL Server عبارتند از self (خود نود)، child (فرزند نود)، parent (والد نود)، decedent (فرزند فرزند فرزند ...)و attribute (دسترسی به ویژگیها). محورهای استانداردی مانند preceding-sibling و following-sibling در SQL Server با عملگرهایی مانند >> و << پشتیبانی میشوند.
مثالهایی از نحوهی استفاده از محورهای XPath
اینبار قصد داریم یک سند XML نسبتا پیچیده را بررسی کرده و اجزای مختلف آنرا به کمک XPath بدست بیاوریم.
DECLARE @doc XML SET @doc=' <Team name="Project 1" xmlns:a="urn:annotations"> <Employee id="544" years="6.5"> <Name>User 1</Name> <Title>Architect</Title> <Expertise>Games</Expertise> <Expertise>Puzzles</Expertise> <Employee id="101" years="7.1" a:assigned-to="C1"> <Name>User 2</Name> <Title>Dev lead</Title> <Expertise>Video Games</Expertise> <Employee id="50" years="2.3" a:assigned-to="C2"> <Name>User 3</Name> <Title>Developer</Title> <Expertise>Hardware</Expertise> <Expertise>Entertainment</Expertise> </Employee> </Employee> </Employee> </Team> '
در XPath، محور پیش فرض، child است (اگر مانند کوئری زیر مورد خاصی ذکر نشود):
SELECT @doc.query('/Team/Employee/Name')
SELECT @doc.query('/Team/Employee/child::Name')
<Name>User 1</Name>
SELECT @doc.query('//Employee/Name')
<Name>User 1</Name> <Name>User 2</Name> <Name>User 3</Name>
برای کار با ویژگیها و attributes از [] به همراه علامت @ استفاده میشود:
SELECT @doc.query(' declare namespace a = "urn:annotations"; //Employee[@a:assigned-to]/Name ')
<Name>User 2</Name> <Name>User 3</Name>
SELECT @doc.query(' declare namespace a = "urn:annotations"; //Employee[attribute::a:assigned-to]/Name ')
SELECT @doc.query(' declare namespace a = "urn:annotations"; //Employee[not(@a:assigned-to)]/Name ')
<Name>User 1</Name>
SELECT @doc.query('count(//Employee[Name="User 1"]/Employee)')
در XPath برای یافتن والد از .. استفاده میشود:
SELECT @doc.query('//Employee[../Name="User 1"]')
استفاده از .. در SQL Server به دلایل کارآیی پایین توصیه نمیشود. بهتر است از همان روش قبلی کوئری تعداد کارمندانی که به user 1 مستقیما گزارش میدهند، استفاده شود.
عبارات FLOWR
FLOWR هستهی XQuery را تشکیل داده و قابلیت توسعه XPath را دارد. FLOWR مخفف for، let، order by، where و retrun است. از for برای تشکیل حلقه، از let برای انتساب، از where و order by برای فیلتر و مرتب سازی اطلاعات و از return برای بازگشت نتایج کمک گرفته میشود. FLOWR بسیار شبیه به ساختار SQL عمل میکند.
معادل عبارت SQL
Select p.name, p.job from people as p where p.age > 30 order by p.age
for $p in /people/person where $p.age > 30 order by $p.age[1] return ($p/name, $p/job)
تنها تفاوت مهم، در اینجا است که در عبارات SQL، خروجی کار توسط select، در ابتدای کوئری ذکر میشود، اما در عبارات FLOWR در انتهای آنها.
از let برای انتساب مجموعهای از نودها استفاده میشود:
let $p := /people/person return $p
یک نکته
اگر به order by دقت کنید، به اولین سن اشاره میکند. Order by در اینجا با تک مقدارها کار میکند و امکان کار با مجموعهای از نودها را ندارد. به همین جهت باید طوری آنرا تنظیم کرد که هربار فقط به یک مقدار اشاره کند.
هر زمانیکه به خطای requires a singleton برخوردید، یعنی دستورات مورد استفاده با یک سری از نودها کار نکرده و نیاز است دقیقا مشخص کنید، کدام مقدار مدنظر است.
مثالهایی از عبارات FLOWR
دو کوئری ذیل یک خروجی 1 2 3 را تولید میکنند
DECLARE @x XML = ''; SELECT @x.query(' for $i in (1,2,3) return $i '); SELECT @x.query(' let $i := (1,2,3) return $i ');
در ادامه اگر سعی کنیم به این کوئریها یک order by را اضافه کنیم، کوئری اول با موفقیت اجرا شده،
DECLARE @x XML = ''; SELECT @x.query(' for $i in (1,2,3) order by $i descending return $i '); SELECT @x.query(' let $i := (1,2,3) order by $i descending return $i ');
XQuery [query()]: 'order by' requires a singleton (or empty sequence), found operand of type 'xs:integer +'
ساخت المانهای جدید XML توسط عبارات FLOWR
ابتدا همان سند XML قسمت قبل را درنظر بگیرید:
DECLARE @doc XML =' <people> <person> <name> <givenName>name1</givenName> <familyName>lname1</familyName> </name> <age>33</age> <height>short</height> </person> <person> <name> <givenName>name2</givenName> <familyName>lname2</familyName> </name> <age>40</age> <height>short</height> </person> <person> <name> <givenName>name3</givenName> <familyName>lname3</familyName> </name> <age>30</age> <height>medium</height> </person> </people> '
SELECT @doc.query(' for $p in /people/person return <person> {$p/name[1]/givenName[1]/text()} </person> ');
<person>name1</person> <person>name2</person> <person>name3</person>
سؤال: اگر به این خروجی بخواهیم یک root element اضافه کنیم، چه باید کرد؟ اگر المان root دلخواهی را در return قرار دهیم، به ازای هر آیتم یافت شده، یکبار تکرار میشود که مدنظر ما نیست.
SELECT @doc.query(' <root> { for $p in /people/person return <person> {$p/name[1]/givenName[1]/text()} </person> } </root> ');
<root> <person>name1</person> <person>name2</person> <person>name3</person> </root>
مفهوم quantification در FLOWR
همان سند Team name=Project 1 ابتدای بحث جاری را درنظر بگیرید.
SELECT @doc.query('some $emp in //Employee satisfies $emp/@years >5') -- true SELECT @doc.query('every $emp in //Employee satisfies $emp/@years >5') -- false
In this video we perform a full step by step build of a .NET MAUI App that we test on both Windows and Android. The app interacts with a separate .NET 6 API that we also build step by step.
Level: Beginner
⏲️ Time Codes ⏲️
Theory
- 0:48 Welcome
- 03:13 App demo
- 06:07 Course overview
- 09:14 Ingedients
- 10:10 What is .NET MAUI?
- 12:48 How MAUI works
- 15:14 MAUI project anatomy
- 19:47 MAUI App start up sequence
- 22:29 UI Conepts
- 28:21 XAML vs C#
- 30:29 Solution Architecture
- 31:41 Application Architecture
API Build
- 35:31 API Project Set up
- 42:41 API Model definition
- 44:47 API Db Context
- 47:13 Connection String
- 52:19 Migrations
- 56:31 API Read Endpoint
- 1:01:58 API Create Endpoint
- 1:08:15 API Update Endpoint
- 1:12:57 API Delete Endpoint
MAUI App Build
- 1:17:21 MAUI App Project Set up
- 1:21:00 Android Device Manager
- 1:25:08 MAUI Model definition
- 1:31:16 Data Service Interface
- 1:35:40 Data Service Implementation
- 1:47:27 Data Service Read Method
- 1:53:34 Data Service Create Method
- 1:58:48 Data Service Delete Method
- 2:01:53 Data Service Update Method
- 2:05:41 Android environment config
- 2:11:00 Architecture check point
- 2:11:54 Register MainPage for DI
- 2:14:13 MainPage code-behind
- 2:21:03 MainPage XAML Layout
- 2:30:19 Re-work MainPage layout
- 2:35:12 Add another page (ManagePage)
- 2:38:01 Adding a Route
- 2:30:01 Regiter ManagePage for DI
- 2:40:29 Complete MainPage code-behind
- 2:45:12 ManagePage code-behind
- 2:51:16 QueryProperty
- 2:57:34 ManagePage XMAL
- 3:07:56 Run on Windows
- 3:09:30 Re-work ManagePage layout
- 3:16:26 Using HttpClientFactory
Outro
- 3:21:02 Wrap up and thanks
- 3:21:31 Supporter Credits
دوره WPF پیشرفته
01 - The Basics
02 - TreeViews and Value Converters
03 - View Model MVVM Basics
04 - Custom Window Chrome and Styles
05 - Creating Login Form Sign Up Screen
06 - Attached Properties
07 - Storyboard Animations
08 - Advanced View Models Real World
09 - User Controls Side Menu Content
10 - ItemsControl Chat List & Design Time Data
11 - Dependency Injection & Multiple Projects
12 - MVVM View Model Binding to Animations
13 - Complete Page Animation
14 - Create Chat Message Bubbles
15 - Adaptive Control Design with View Model Binding
16 - Chat Message Title Bar Menu
17 - Chat Message Input Box Control
18 - Styling Scrollbars Custom
19 - Creating Popup Menus
20 - Creating Vertical Popup Menu
21 - Custom Dialog System Message Box Popup
22 - Slide Up Settings Menu
23 - Settings Page UI
24 - Advanced Edit Text Control
25 - Advanced Cross-Control Syncing
پشتیبانی از Generic Attributes در C# 11
/// <summary> /// Instantiates a new <see cref="ServiceFilterAttribute"/> instance. /// </summary> /// <param name="type">The <see cref="Type"/> of filter to find.</param> public ServiceFilterAttribute(Type type) { ServiceType = type ?? throw new ArgumentNullException(nameof(type)); }
public class TypeSafeServiceFilterAttribute<T> : ServiceFilterAttribute where T: IActionFilter { public TypeSafeServiceFilterAttribute():base(typeof(T)) { } }
[Route("api/[controller]")] [ApiController] public class CoursesController : ControllerBase { [HttpGet] [TypeSafeServiceFilterAttribute<ExampleFilter>()] public IActionResult Get() { return Ok(); } }
دوره 13 ساعته زبان Rust
Learn Rust Programming - Complete Course 🦀
In this comprehensive Rust course for beginners, you will learn about the core concepts of the language and underlying mechanisms in theory.
⭐️ Contents ⭐️
00:00:00 Introduction & Learning Resources
00:06:19 Variables
00:27:07 Numbers & Binary System
01:09:51 Chars, Bools & Unit Types
01:17:55 Statements & Expressions
01:24:50 Functions
01:32:53 Ownership
02:24:06 Borrowing
02:47:45 String vs. &str
03:17:59 Slices
03:31:35 Tuples
03:40:04 Structs
04:02:52 Enums
04:13:46 The "Option" Enum
04:21:32 Flow Control
04:44:43 Pattern Match
05:16:42 Methods & Associated Functions
05:31:50 Generics
06:06:32 Traits
06:47:15 Trait Objects
07:09:51 Associated Types
07:39:31 String
07:59:52 Vectors
08:29:00 HashMaps
08:52:45 Type Coercion
09:04:54 From & Into
09:36:03 panic!
09:44:56 Result
10:28:23 Cargo, Crates & Modules
11:08:28 Debug & Display
11:30:13 Lifetimes
12:14:46 Lifetime Elision
12:38:53 Closures
13:30:08 Iterators
مدیریت تغییرات گریدی از اطلاعات به کمک استفاده از الگوی واحد کار مشترک بین ViewModel و لایه سرویس
در این صفحه با کلیک بر روی دکمه به علاوه، یک ردیف به ردیفهای موجود اضافه شده و در اینجا میتوان اطلاعات کاربر جدیدی به همراه سطح دسترسی او را وارد و ذخیره کرد و یا حتی اطلاعات کاربران موجود را ویرایش نمود. اگر بخواهیم مانند مراحلی که در قسمت قبل در مورد تعریف یک صفحه جدید در برنامه توضیح داده شد، عمل کنیم، به صورت خلاصه به ترتیب ذیل عمل شده است:
1) ایجاد صفحه تغییر مشخصات کاربر
ابتدا صفحه Views\Admin\AddNewUser.xaml به پروژه ریشه که Viewهای برنامه در آن تعریف میشوند، اضافه شده است. به همراه دو دکمه و یک ListView که تطابق بهتری با قالب متروی مورد استفاده دارد.
2) تنظیم اعتبارسنجی صفحه اضافه شده
مرحله بعد تعریف هر صفحهای در سیستم، مشخص سازی وضعیت دسترسی به آن است:
/// <summary> /// افزودن و مدیریت کاربران سیستم /// </summary> [PageAuthorization(AuthorizationType.ApplyRequiredRoles, "IsAdmin, CanAddNewUser")]
3) تغییر منوی برنامه جهت اشاره به صفحه جدید
در ادامه در فایل منوی برنامه Views\MainMenu.xaml تعریف دسترسی به صفحه Views\Admin\AddNewUser.xaml قید شده است:
<Button Style="{DynamicResource MetroCircleButtonStyle}" Height="55" Width="55" Command="{Binding DoNavigate}" CommandParameter="\Views\Admin\AddNewUser.xaml" Margin="2"> <Rectangle Width="28" Height="17.25"> <Rectangle.Fill> <VisualBrush Stretch="Fill" Visual="{StaticResource appbar_user_add}" /> </Rectangle.Fill> </Rectangle> </Button>
4) ایجاد ViewModel متناظر با صفحه
مرحله نهایی تعریف صفحه AddNewUser، افزودن ViewModel متناظر با آن است که سورس کامل آنرا در فایل ViewModels\Admin\AddNewUserViewModel.cs پروژه Infrastructure میتوانید ملاحظه کنید.
نکته مهم این ViewModel، ارائه خاصیت لیست کاربران از نوع ObservableCollection به View و گرید برنامه است:
public ObservableCollection<User> UsersList { set; get; }
/// <summary> /// جهت مقاصد انقیاد دادهها در دبلیو پی اف طراحی شده است /// لیستی از کاربران سیستم را باز میگرداند /// </summary> /// <param name="count">تعداد کاربر مد نظر</param> /// <returns>لیستی از کاربران</returns> public ObservableCollection<User> GetSyncedUsersList(int count = 1000) { _users.OrderBy(x => x.FriendlyName).Take(count) .Load(); // For Databinding with WPF. // Before calling this method you need to fill the context by using `Load()` method. return _users.Local; }
در اینجا از قابلیت خاصیتی به نام Local که یک ObservableCollection تحت نظر EF را بازگشت میدهد، استفاده شده است. برای استفاده از این خاصیت، ابتدا باید کوئری خود را تهیه و سپس متد Load را بر روی آن فراخوانی کرد. سپس خاصیت Local بر اساس اطلاعات کوئری قبلی پر و مقدار دهی خواهد شد.
علت انتخاب نام Synced برای این متد، تحت نظر بودن اطلاعات خاصیت Local است تا زمانیکه Context تعریف شده زنده نگه داشته شود. به همین جهت در برنامه جاری از روش زنده نگه داشتن Context به ازای یک ViewModel استفاده شده است.
به Context، توسط اینترفیس IUnitOfWork تزریق شده در سازنده کلاس ViewModel میتوان دسترسی یافت. چون در اینجا از تزریق وابستگیها استفاده شده است، وهلهای که IUnitOfWork کلاس AddNewUserViewModel را تشکیل میدهد، دقیقا همان وهلهای است که در کلاس UsersService لایه سرویس استفاده شده است. در نتیجه، در گرید برنامه هر تغییری اعمال شود، تحت نظر IUnitOfWork خواهد بود و صرفا با فراخوانی متد uow.ApplyAllChanges آن، کلیه تغییرات تمام ردیفهای تحت نظر EF به صورت خودکار در بانک اطلاعاتی درج و یا به روز خواهند شد.
همچنین در مورد ViewModelContextHasChanges نیز در قسمت قبل بحث شد. در اینجا پیاده سازی کننده آن صرفا خاصیت uow.ContextHasChanges است. به این ترتیب اگر کاربر، تغییری را در صفحه داده باشد و بخواهد به صفحه دیگری رجوع کند، با پیام زیر مواجه خواهد شد:
از همین خاصیت برای فعال و غیرفعال کردن دکمه ذخیره سازی اطلاعات نیز استفاده شده است:
/// <summary> /// فعال و غیرفعال سازی خودکار دکمه ثبت /// این متد به صورت خودکار توسط RelayCommand کنترل میشود /// </summary> private bool canDoSave() { // آیا در حین نمایش صفحهای دیگر باید به کاربر پیغام داد که اطلاعات ذخیره نشدهای وجود دارد؟ return ViewModelContextHasChanges; }
/// <summary> /// رخداد ذخیره سازی اطلاعات را دریافت میکند /// </summary> public RelayCommand DoSave { set; get; }
DoSave = new RelayCommand(doSave, canDoSave);
این بررسی نیز بسیار سبک و سریع است. از این جهت که تغییرات Context در حافظه نگهداری میشوند و مراجعه به آن مساوی مراجعه به بانک اطلاعاتی نیست.
- تنظیمات برنامه را از چندین منبع مختلف خوانده و آنها را یکی کنید.
- تنظیمات را بر اساس تنظیمات مختلف محیطی برنامه، بارگذاری کنید.
- امکان نگاشت اطلاعات خوانده شدهی از فایلهای کانفیگ به کلاسها پیش بینی شدهاست.
- امکان بارگذاری مجدد فایلهای کانفیگ درصورت تغییر، بدون ریاستارت کل برنامه وجود دارد.
- امکان تزریق وابستگیهای تنظیمات برنامه، به قسمتهای مختلف آن پیش بینی شدهاست.
نصب پیشنیاز خواندن تنظیمات برنامه از یک فایل JSON
برای شروع به کار با خواندن تنظیمات برنامه در ASP.NET Core، نیاز است ابتدا بستهی نیوگت Microsoft.Extensions.Configuration.Json را نصب کنیم.
برای این منظور بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.Extensions.Configuration.Json را جستجو کرده و نصب نمائید:
البته همانطور که در تصویر مشاهده میکنید، اگر صرفا Microsoft.Extensions.Configuration را جستجو کنید (بدون ذکر JSON)، بستههای مرتبط با خواندن فایلهای کانفیگ از نوع XML و یا حتی INI را هم خواهید یافت.
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{ "dependencies": { //same as before "Microsoft.Extensions.Configuration.Json": "1.0.0" },
افزودن یک فایل کانفیگ JSON دلخواه
بر روی پروژه کلیک راست کرده و از طریق منوی add->new item یک فایل خالی جدید را به نام appsettings.json ایجاد کنید (نام این فایل دلخواه است)؛ با این محتوا:
{ "Key1": "Value1", "Auth": { "Users": [ "Test1", "Test2", "Test3" ] }, "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } }
<appSettings> <add key="Logging-IncludeScopes" value="false" /> <add key="Logging-Level-Default" value="verbose" /> <add key="Logging-Level-System" value="Information" /> <add key="Logging-Level-Microsoft" value="Information" /> </appSettings>
در فایل JSON فوق، نمونهای از key/valueها، آرایهها و اطلاعات چندین سطحی را مشاهده میکنید.
خواندن فایل تنظیمات appsettings.json در برنامه
پس از نصب پیشنیاز خواندن فایلهای کانفیگ از نوع JSON، به فایل آغازین برنامه مراجعه کرده و سازندهی جدیدی را به آن اضافه کنید:
public class Startup { public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json"); Configuration = builder.Build(); }
الف) این خواندن، در سازندهی کلاس آغازین برنامه و پیش از تمام تنظیمات دیگر باید انجام شود.
ب) جهت در معرض دید قرار دادن اطلاعات خوانده شده، آنرا به یک خاصیت عمومی انتساب دادهایم.
ج) متد SetBasePath جهت مشخص کردن محل یافتن فایل appsettings.json ذکر شدهاست. این اطلاعات را میتوان از سرویس توکار IHostingEnvironment و خاصیت ContentRootPath آن دریافت کرد. همانطور که ملاحظه میکنید، این تزریق وابستگی نیز به صورت خودکار توسط ASP.NET Core مدیریت میشود.
دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot
تا اینجا موفق شدیم تا تنظیمات خوانده شده را به خاصیت عمومی Configuration از نوع IConfigurationRoot انتساب دهیم. اما ساختار ذخیره شدهی در این اینترفیس به چه صورتی است؟
همانطور که مشاهده میکنید، هر سطح از سطح قبلی آن با : جدا شدهاست. همچنین اعضای آرایه، دارای ایندکسهای 0: و 1: و 2: هستند. بنابراین برای خواندن این اطلاعات میتوان نوشت:
var key1 = Configuration["Key1"]; var user1 = Configuration["Auth:Users:0"]; var authUsers = Configuration.GetSection("Auth:Users").GetChildren().Select(x => x.Value).ToArray(); var loggingIncludeScopes = Configuration["Logging:IncludeScopes"]; var loggingLoggingLogLevelDefault = Configuration["Logging:LogLevel:Default"];
یک نکته: خاصیت Configuration، دارای متد GetValue نیز هست که توسط آن میتوان نوع مقدار دریافتی و یا حتی مقدار پیش فرضی را در صورت عدم وجود این key، مشخص کرد:
var val = Configuration.GetValue<int>("key-name", defaultValue: 10);
سرویس IConfigurationRoot قابل تزریق است
در قسمت قبل، سرویسها و تزریق وابستگیها را بررسی کردیم. نکتهی جالبی را که میتوان به آن اضافه کرد، قابلیت تزریق خاصیت عمومی
public class Startup { public IConfigurationRoot Configuration { set; get; }
الف) اعلام موجودیت IConfigurationRoot به IoC Container
اگر از استراکچرمپ استفاده میکنید، باید مشخص کنید، زمانیکه IConfigurationRoot درخواست شد، آنرا چگونه باید از خاصیت مرتبط با آن دریافت کند:
var container = new Container(); container.Configure(config => { config.For<IConfigurationRoot>().Singleton().Use(() => Configuration);
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
ب) فایل project.json کتابخانهی Core1RtmEmptyTest.Services را گشوده و وابستگی Microsoft.Extensions.Configuration.Abstractions را به آن اضافه کنید:
{ "dependencies": { //same as before "Microsoft.Extensions.Configuration.Abstractions": "1.0.0" }
ج) سپس فایل MessagesService.cs را گشوده و این اینترفیس را به سازندهی سرویس MessagesService تزریق میکنیم:
public interface IMessagesService { string GetSiteName(); } public class MessagesService : IMessagesService { private readonly IConfigurationRoot _configurationRoot; public MessagesService(IConfigurationRoot configurationRoot) { _configurationRoot = configurationRoot; } public string GetSiteName() { var key1 = _configurationRoot["Key1"]; return $"DNT {key1}"; } }
اکنون اگر برنامه را اجرا کنید، با توجه به اینکه میان افزار Run از این سرویس سفارشی استفاده میکند:
public void Configure( IApplicationBuilder app, IHostingEnvironment env, IMessagesService messagesService) { app.Run(async context => { var siteName = messagesService.GetSiteName(); await context.Response.WriteAsync($"Hello {siteName}"); }); }
خواندن تنظیمات از حافظه
الزاما نیازی به استفاده از فایلهای JSON و یا XML در اینجا وجود ندارد. ابتداییترین حالت کار با بستهی Microsoft.Extensions.Configuration، متد AddInMemoryCollection آن است که در اینجا میتوان لیستی از key/valueها را ذکر کرد:
var builder = new ConfigurationBuilder() .AddInMemoryCollection(new[] { new KeyValuePair<string,string>("the-key", "the-value"), });
var theValue = Configuration["the-key"];
امکان بازنویسی تنظیمات انجام شده، بسته به شرایط محیطی
در اینجا محدود به یک فایل JSON و یک فایل تنظیمات برنامه، نیستیم. برای کار با ConfigurationBuilder میتوان از Fluent interface آن استفاده کرد و به هر تعدادی که نیاز بود، متدهای خواندن از فایلهای کانفیگ دیگر را اضافه کرد:
public class Startup { public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddInMemoryCollection(new[] { new KeyValuePair<string,string>("the-key", "the-value"), }) .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false) .AddJsonFile($"appsettings.{env}.json", optional: true); Configuration = builder.Build(); }
برای مثال در اینجا آخرین AddJsonFile تعریف شده، بنابر متغیر محیطی فعلی به appsettings.development.json تفسیر شده و در صورت وجود این فایل (با توجه به optional بودن آن) اطلاعات آن دریافت گردیده و اطلاعات مشابه فایل appsettings.json قبلی را بازنویسی میکند.
امکان دسترسی به متغیرهای محیطی سیستم عامل
در انتهای زنجیرهی ConfigurationBuilder میتوان متد AddEnvironmentVariables را نیز ذکر کرد:
var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) .AddEnvironmentVariables();
امکان نگاشت تنظیمات برنامه به کلاسهای متناظر
کار کردن با key/valueهای رشتهای، هرچند روش پایهای استفادهی از تنظیمات برنامه است، اما آنچنان refactoring friendly نیست. در ASP.NET Core امکان تعریف تنظیمات strongly typed نیز پیش بینی شدهاست. برای این منظور باید مراحل زیر طی شوند:
به عنوان نمونه تنظیمات فرضی smtp ذیل را به انتهای فایل appsettings.json اضافه کنید:
{ "Key1": "Value1", "Auth": { "Users": [ "Test1", "Test2", "Test3" ] }, "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } }, "Smtp": { "Server": "0.0.0.1", "User": "user@company.com", "Pass": "123456789", "Port": "25" } }
در این کتابخانهی جدید که محل نگهداری ViewModelهای برنامه خواهد بود، کلاس معادل قسمت smtp فایل config فوق را اضافه کنید:
namespace Core1RtmEmptyTest.ViewModels { public class SmtpConfig { public string Server { get; set; } public string User { get; set; } public string Pass { get; set; } public int Port { get; set; } } }
سپس به پروژهی Core1RtmEmptyTest مراجعه کرده و بر روی گره references آن کلیک راست کنید. در اینجا گزینهی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید، تا اسمبلی آنرا بتوان در پروژهی جاری استفاده کرد.
انجام اینکار معادل است با افزودن یک سطر ذیل به فایل project.json پروژه:
{ "dependencies": { // same as before "Core1RtmEmptyTest.ViewModels": "1.0.0-*" },
و سپس در کلاس آغازین برنامه و متد ConfigureServices آن، نحوهی نگاشت قسمت Smtp فایل کانفیگ را مشخص کنید:
public IServiceProvider ConfigureServices(IServiceCollection services) { services.Configure<SmtpConfig>(options => Configuration.GetSection("Smtp").Bind(options));
سپس برای استفاده از این تنظیمات strongly typed (برای نمونه در لایه سرویس برنامه)، ابتدا ارجاعی را به پروژهی Core1RtmEmptyTest.ViewModels به لایهی سرویس برنامه اضافه میکنیم (بر روی گره references آن کلیک راست کنید. در اینجا گزینهی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید).
در ادامه نیاز است بستهی نیوگت جدیدی را به نام Microsoft.Extensions.Options به لایهی سرویس برنامه اضافه کنیم. به این ترتیب قسمت وابستگیهای فایل project.json این لایه چنین شکلی را پیدا میکند:
"dependencies": { "Core1RtmEmptyTest.ViewModels": "1.0.0-*", "Microsoft.Extensions.Configuration.Abstractions": "1.0.0", "Microsoft.Extensions.Options": "1.0.0", "NETStandard.Library": "1.6.0" }
public interface IMessagesService { string GetSiteName(); } public class MessagesService : IMessagesService { private readonly IConfigurationRoot _configurationRoot; private readonly IOptions<SmtpConfig> _settings; public MessagesService(IConfigurationRoot configurationRoot, IOptions<SmtpConfig> settings) { _configurationRoot = configurationRoot; _settings = settings; } public string GetSiteName() { var key1 = _configurationRoot["Key1"]; var server = _settings.Value.Server; return $"DNT {key1} - {server}"; } }
اکنون اگر برنامه را جرا کنید، این خروجی را میتوان مشاهده کرد (که در آن آدرس Server دریافت شدهی از فایل کانفیگ نیز مشخص است):
البته همانطور که در قسمت قبل نیز عنوان شد، این تزریق وابستگیها در تمام قسمتهای برنامه کار میکند. برای مثال در کنترلرها هم میتوان <IOptions<SmtpConfig را به همین نحو تزریق کرد.
نحوهی واکنش به تغییرات فایلهای کانفیگ
در نگارشهای قبلی ASP.NET، هر تغییری در فایل web.config، سبب ریاستارت شدن کل برنامه میشد که این مساله نیز خود سبب بروز مشکلات زیادی مانند از دست رفتن سشن تمام کاربران میشد.
در ASP.NET Core، برنامهی وب ما دیگر متکی به فایل web.config نبوده و همچنین میتوان چندین و چند نوع فایل config داشت. به علاوه در اینجا متدهای مرتبط معرفی فایلهای کانفیگ دارای پارامتر مخصوص reloadOnChange نیز هستند:
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
معرفی ویژگی جدید StringSyntax
با استفاده از ویژگی StringSyntax جدید میتوان مقدار مورد انتظار از رشتهی درخواستی را معنادار کرد. برای مثال، Visual Studio سالهاست که راهنمایی را در حین تعریف عبارات باقاعده ظاهر میکند. اما این راهنما صرفا مختص به ویژوال استودیو است و تا پیش از این راهی وجود نداشت تا عنوان کنیم که برای مثال این رشته قرار است تنها یک عبارت باقاعده باشد. اکنون در دات نت 7 با معرفی ویژگی جدید StringSyntax میتوان یک چنین intellisense ای را در سایر IDEها نیز شاهد بود.
برای نمونه مثال زیر را درنظر بگیرید:
using System.Diagnostics.CodeAnalysis; namespace CS11Tests; public class StringSyntaxAttributeTests { public static void Test() { RegexTest(""); DateTest(""); } private static void RegexTest([StringSyntax(StringSyntaxAttribute.Regex)] string regex) { } private static void DateTest([StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string dateTime) { } }
راهنمای ظاهر شده جهت تعریف سادهتر عبارات باقاعده:
و راهنمای ظاهر شده جهت تعریف سادهتر یک DateTime:
امکان استفاده از StringSyntax در دات نتهای پیش از نگارش 7
هرچند StringSyntax در دات نت 7 تعریف شدهاست؛ اما اگر تعریف کلاس زیر را به همراه فضای نام دقیق آن به پروژههای قدیمیتر هم اضافه کنیم ... برای دات نتهای پیش از نگارش 7 هم کار میکند:
#if !NET7_0_OR_GREATER namespace System.Diagnostics.CodeAnalysis { /// <summary>Specifies the syntax used in a string.</summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] public sealed class StringSyntaxAttribute : Attribute { /// <summary>The syntax identifier for strings containing composite formats for string formatting.</summary> public const string CompositeFormat = "CompositeFormat"; /// <summary>The syntax identifier for strings containing date format specifiers.</summary> public const string DateOnlyFormat = "DateOnlyFormat"; /// <summary>The syntax identifier for strings containing date and time format specifiers.</summary> public const string DateTimeFormat = "DateTimeFormat"; /// <summary>The syntax identifier for strings containing <see cref="T:System.Enum" /> format specifiers.</summary> public const string EnumFormat = "EnumFormat"; /// <summary>The syntax identifier for strings containing <see cref="T:System.Guid" /> format specifiers.</summary> public const string GuidFormat = "GuidFormat"; /// <summary>The syntax identifier for strings containing JavaScript Object Notation (JSON).</summary> public const string Json = "Json"; /// <summary>The syntax identifier for strings containing numeric format specifiers.</summary> public const string NumericFormat = "NumericFormat"; /// <summary>The syntax identifier for strings containing regular expressions.</summary> public const string Regex = "Regex"; /// <summary>The syntax identifier for strings containing time format specifiers.</summary> public const string TimeOnlyFormat = "TimeOnlyFormat"; /// <summary>The syntax identifier for strings containing <see cref="T:System.TimeSpan" /> format specifiers.</summary> public const string TimeSpanFormat = "TimeSpanFormat"; /// <summary>The syntax identifier for strings containing URIs.</summary> public const string Uri = "Uri"; /// <summary>The syntax identifier for strings containing XML.</summary> public const string Xml = "Xml"; /// <summary>Initializes the <see cref="T:System.Diagnostics.CodeAnalysis.StringSyntaxAttribute" /> with the identifier of the syntax used.</summary> /// <param name="syntax">The syntax identifier.</param> public StringSyntaxAttribute(string syntax) { this.Syntax = syntax; this.Arguments = Array.Empty<object>(); } /// <summary>Initializes the <see cref="T:System.Diagnostics.CodeAnalysis.StringSyntaxAttribute" /> with the identifier of the syntax used.</summary> /// <param name="syntax">The syntax identifier.</param> /// <param name="arguments">Optional arguments associated with the specific syntax employed.</param> public StringSyntaxAttribute(string syntax, params object?[] arguments) { this.Syntax = syntax; this.Arguments = arguments; } /// <summary>Gets the identifier of the syntax used.</summary> public string Syntax { get; } /// <summary>Gets the optional arguments associated with the specific syntax employed.</summary> public object?[] Arguments { get; } } } #endif