PM> Install-Package LightInject
PM> Install-Package LightInject.Source
public class PersonModel { public int Id { get; set; } public string Name { get; set; } public string Family { get; set; } public DateTime Birth { get; set; } } public interface IRepository<T> where T:class { void Insert(T entity); IEnumerable<T> FindAll(); } public interface IPersonRepository:IRepository<PersonModel> { } public class PersonRepository:IPersonRepository { public void Insert(PersonModel entity) { throw new NotImplementedException(); } public IEnumerable<PersonModel> FindAll() { throw new NotImplementedException(); } } public interface IPersonService { void Insert(PersonModel entity); IEnumerable<PersonModel> FindAll(); } public class PersonService:IPersonService { private readonly IPersonRepository _personRepository; public PersonService(IPersonRepository personRepository) { _personRepository = personRepository; } public void Insert(PersonModel entity) { _personRepository.Insert(entity); } public IEnumerable<PersonModel> FindAll() { return _personRepository.FindAll(); } }
public partial class Form1 : Form { private readonly IPersonService _personService; public Form1(IPersonService personService) { _personService = personService; InitializeComponent(); } }
static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); var container = new ServiceContainer(); container.Register<IPersonService, PersonService>(); container.Register<IPersonRepository, PersonRepository>(); Application.Run(new Form1(container.GetInstance<IPersonService>())); }
public class WorkerModel:PersonModel { public ManagerModel Manager { get; set; } } public class ManagerModel:PersonModel { public IEnumerable<WorkerModel> Workers { get; set; } } public class WorkerRepository:IPersonRepository { public void Insert(PersonModel entity) { throw new NotImplementedException(); } public IEnumerable<PersonModel> FindAll() { throw new NotImplementedException(); } } public class ManagerRepository:IPersonRepository { public void Insert(PersonModel entity) { throw new NotImplementedException(); } public IEnumerable<PersonModel> FindAll() { throw new NotImplementedException(); } } public class WorkerService:IPersonService { private readonly IPersonRepository _personRepository; public WorkerService(IPersonRepository personRepository) { _personRepository = personRepository; } public void Insert(PersonModel entity) { var worker = entity as WorkerModel; _personRepository.Insert(worker); } public IEnumerable<PersonModel> FindAll() { return _personRepository.FindAll(); } } public class ManagerService:IPersonService { private readonly IPersonRepository _personRepository; public ManagerService(IPersonRepository personRepository) { _personRepository = personRepository; } public void Insert(PersonModel entity) { var manager = entity as ManagerModel; _personRepository.Insert(manager); } public IEnumerable<PersonModel> FindAll() { return _personRepository.FindAll(); } }
... var container = new ServiceContainer(); container.Register<IPersonService, PersonService>(); container.Register<IPersonService, WorkerService>(); container.Register<IPersonRepository, PersonRepository>(); container.Register<IPersonRepository, WorkerRepository>(); Application.Run(new Form1(container.GetInstance<IPersonService>()));
... container.Register<IPersonService, PersonService>("PersonService"); container.Register<IPersonService, WorkerService>(); container.Register<IPersonRepository, PersonRepository>(); container.Register<IPersonRepository, WorkerRepository>(); Application.Run(new Form1(container.GetInstance<IPersonService>("PersonService")));
container.Register<IPersonService, PersonService>("PersonService"); Application.Run(new Form1(container.GetInstance<IPersonService>()));
container.Register<IPersonService, PersonService>(); container.Register<IPersonService, WorkerService>("WorkerService"); var personList = container.GetInstance<IEnumerable<IPersonService>>();
container.Register<IPersonService, PersonService>(); container.Register<IPersonService, WorkerService>("WorkerService"); var personList = container.GetAllInstances<IPersonService>();
- Array
- ICollection<T>
- IList<T>
- IReadOnlyCollection<T>
- IReadOnlyList<T>
container.RegisterInstance<string>("SomeValue"); var value = container.GetInstance<string>();
container.RegisterInstance<string>("SomeValue","String1"); container.RegisterInstance<string>("OtherValue","String2"); var value = container.GetInstance<string>("String2");
یک سالی میشود که روی یک فریمورک رابط کاربری بصورت
متن باز به اسم HandyControl مشارکت دارم، این فریمورک در درجه اول مخصوص
برنامه نویسهای چین طراحی شده بود اما با بازخوردهایی که دریافت کرد،
جامعه انگلیسی زبان را هم پشتیبانی میکند. بدلیل اینکه هدف این فریمورک
ارائه کنترلهای ساده و بدور از پیچیدگی هست، Nabian با
اضافه کردن کنترلهای پیچیده مخالف هست. به همین دلیل تصمیم گرفتم تا شاخه
جدایی از این پروژه را ایجاد کنم و کنترلها و ویژگیهای مختلفی که سازگار
با برنامه نویسهای ایرانی باشد را اضافه کنم. درحال حاضر تقویم شمسی،
ساعت_تقویم شمسی، پشتیبانی از کد ملی، حروف فارسی در نوع ورودی TextBox
،استایلهای جدید بارگذاری، کنترل BusyIndicator، کنترل سرعت شمار
(SpeedoMeter) و چندین کلاس هلپر جهت کار با رجیستری، رمزنگاری و... را به
پروژه اضافه کردم.(لیست ویژگیهای اضافه شده را میتوانید از این قسمت مشاهده
کنید) (جهت مشاهده کنترلها و ویژگیهای اضافه شده در نسخه شخصی سازی شده
بعد از اجرای دمو از بخش Controls بخش PersianToolkit را انتخاب کنید)
صفحات پروژه اصلی===> سورس برنامه | صفحه ناگت | صفحه مارکت پلیس | مستندات چینی | مستندات انگلیسی | مستندات ویکی
مقدمه
در اکثر موارد در یک Landscape عملیاتی، چنانچه به تجمیع و انتقال دادهها از بانکهای اطلاعاتی مختلف نیاز باشد، از SSIS Package اختصار (SQL Server Integration Service) استفاده میشود و معمولاً با تعریف یک Job در سطح SQL Server به اجرای Package در زمانهای مشخص میپردازند. چنانچه در موقعیتی لازم باشد که از طریق برنامه کاربردی توسعه یافته، به اجرای Package مبادرت ورزیده شود و البته نخواهیم Job تعریف شده را از طریق کد برنامه، اجرا کنیم و در واقع این امکان را داشته باشیم که همانند یک رویه ذخیره شده تعریف شده در سطح بانک اطلاعاتی به اجرای عمل فوق بپردازیم، یک راه حل میتواند تعریف یک CLR Stored Procedures باشد. در این مقاله به بررسی این موضوع پرداخته میشود، در ابتدا لازم است به بیان تئوری موضوع پرداخته شود (قسمتهای 1 الی 5) در ادامه به ذکر پیاده سازی روش پیشنهادی پرداخته میشود.
1- اجرای Integration Service Package
جهت اجرای یک Package از ابزارهای زیر میتوان استفاده کرد:• command-line ابزار خط فرمان dtexec.exeتوجه: همچنین یک Package را در زمان طراحی در Business Intelligence Development Studio) BIDS) میتوان اجرا نمود.
• ابزار اجرائی پکیج dtexecui.exe
• استفاده از SQL Server Agent job
2- استفاده از dtexec جهت اجرای Package
با استفاده از ابزار dtexec میتوان Packageهای ذخیره شده در فایل سیستم، یک SQL Instance و یا Packageهای ذخیره شده در Integration Service را اجرا نمود.توجه: در سیستم عاملهای 64 بیتی، ابزار dtexec موجود در Integration Service با نسخه 64 بیتی نصب میشود. چنانچه بایست Packageهای معینی را در حالت 32 بیتی اجرا کنید، لازم است ابزار dtexec نسخه 32 بیتی نصب شود. ابزار dtexec دستیابی به تمامی ویژگیهای پیکربندی و اجرای Package از قبیل اتصالات، مشخصات(Properties)، متغیرها، logging و شاخصهای پردازشی را فراهم میکند.
توجه: زمانی که از نسخهی ابزار dtexec که با SQL Server 2008 ارائه شده استفاده میکنید برای اجرای یک SSIS Package نسخه 2005، Integration Service به صورت موقت Package را به نسخه 2008 ارتقا میدهد، اما نمیتوان از ابزار dtexec برای ذخیره این تغییرات استفاده کرد.
2-1- ملاحظات نصب dtexec روی سیستمهای 64 بیتی
به صورت پیش فرض، یک سیستم عامل 64 بیتی که هر دو نسخه 64 بیتی و 32 بیتی ابزار خط فرمان Integration Service را دارد، نسخه 32 بیتی نصب شده را در خط فرمان اجرا خواهد کرد. نسخه 32 بیتی بدین دلیل اجرا میشود که در متغیر محیطی (Path (Path environment variable مسیر directory نسخه 32 بیتی قرار گرفته است.به طور معمول:2-2- تفسیر کدهای خروجی
هنگامی که یک Package اجرا میشود، dtexec یک کد خروجی (Return Code) بر میگرداند:مقدار | توصیف |
0 | Package با موفقیت اجرا شده است. |
1 | Package با خطا مواجه شده است. |
3 | Package در حال اجرا توسط کاربر لغو شده است. |
4 | Package پیدا نشده است. |
5 | Package بارگذاری نشده است. |
6 | ابزار با یک خطای نحوی یا خطای معنایی در خط فرمان برخورد کرده است. |
2-3- قوانین نحوی dtexec
تمامی گزینهها (Options) باید با یک علامت Slash (/) و یا Minus (-) شروع شوند.یک آرگومان باید در یک quotation mark محصور شود چنانچه شامل یک فاصله خالی باشد.
گزینهها و آرگومانها بجز رمزعبور حساس به حروف کوچک و بزرگ نیستند.
2-3-1- Syntax
dtexec /option [value] [/option [value]]…
2-3-2- Parameters
نکته: در Integration Service، ابزار خط فرمان dtsrun که برایData Transformation Service) DTS)های نسخه SQL Server 2000 استفاده میشد، با ابزار خط فرمان dtexec جایگزین شده است.• تعدادی از گزینههای خط فرمان dtsrun به طور مستقیم در dtexec معادل دارند برای مثال نام Server و نام Package.
• تعدادی از گزینههای dtsrun به طور مستقیم در dtexec معادل ندارند.
• تعدادی گزینههای خط فرمان جدید dtsexec وجود دارد که در ویژگیهای جدید Integration Service پشتیبانی میشود.
2-3-3- مثال
1) به منظور اجرای یک SSIS Package که در SQL Server ذخیره شده است، با استفاده از Windows Authentication :dtexec /sq <Package Name> /ser <Server Name>
2) به منظور اجرای یک SSIS Package که در پوشه File System در SSIS Package Store ذخیره شده است :
dtexec /dts “\File System\<Package File Name>”
3) به منظور اجرای یک SSIS Package که در سیستم فایل ذخیره شده است و مشخص کردن گزینه logging:
dtexec /f “c:\<Package File Name>” /l “DTS.LogProviderTextFile; <Log File Name>”
4) به منظور اجرای یک SSIS Package که در SQL Server ذخیره شده با استفاده از SQL Server Authentication برای نمونه(user:ssis;pwd:ssis@ssis)و رمز (Package(123:
dtexec /server “<Server Name>” /sql “<Package Name>” / user “ssis” /Password “ssis@ssis” /De “123”
3- تنظیمات سطح حفاظتی یک Package
به منظور حفاظت از دادهها در Packageهای Integration Service میتوانید یک سطح حفاظتی (protection level) را تنظیم کنید که به حفاظت از دادههای صرفاً حساس یا تمامی دادههای یک Package کمک نماید. به علاوه میتوانید این دادهها را با یک Password یا یک User Key رمزگذاری نمائید یا به رمزگذاری دادهها در بانک اطلاعاتی اعتماد کنید. همچنین سطح حفاظتی که برای یک Package استفاده میکنید، الزاماً ایستا (static) نیست و در طول چرخه حیات یک Package میتواند تغییر کند. اغلب سطح حفاظتی در طول توسعه یا به محض (deploy) استقرار Package تنظیم میشود.توجه: علاوه بر سطوح حفاظتی که توصیف شد، Packageها در بانک اطلاعاتی msdb ذخیره میشوند که همچنین میتوانند توسط نقشهای ثابت در سطح بانک اطلاعاتی (fixed database-level roles) حفاظت شوند. Integration Service شامل 3 نقش ثابت بانک اطلاعاتی برای نسبت دادن مجوزها به Package است که عبارتند از db_ssisadmin ،db_ssisltduser و db_ssisoperator
3-1- درک سطوح حفاظتی
در یک Package اطلاعات زیر به عنوان حساس تعریف میشوند:• بخش password در یک connection string. گرچه، اگر گزینه ای را که همه چیز را رمزگذاری کند، انتخاب کنید تمامی connection string حساس در نظر گرفته میشود.
• گرههای task-generated XML که برچسب (tagged) هایی حساس هستند.
• هر متغییری که به عنوان حساس نشان گذاری شود.
3-1-1- Do not save sensitive
هنگامی که Package ذخیره میشود از ذخیره مقادیر ویژگیهای حساس در Package جلوگیری میکند. این سطح حفاظتی رمزگذاری نمیکند اما در عوض از ذخیره شدن ویژگی هایی که حساس نشان گذاری شده اند به همراه Package جلوگیری میکند.3-1-2- Encrypt all with password
به منظور رمزگذاری تمامی Package از یک Password استفاده میشود. Package توسط Password ای رمزگذاری میشود که کاربر هنگامی که Package را ایجاد یا Export میکند، ارائه میدهد. به منظور باز کردن Package در SSIS Designer یا اجرای Package توسط ابزار خط فرمان dtexec کاربر بایست رمز Package را ارائه نماید. بدون رمز کاربر قادر به دستیابی و اجرای Package نیست.3-1-3- Encrypt all with user key
به منظور رمزگذاری تمامی Package از یک کلید که مبتنی بر Profile کاربر جاری میباشد، استفاده میشود. تنها کاربری که Package را ایجاد یا Export میکند، میتواند Package را در SSIS Designer باز کند و یا Package را توسط ابزار خط فرمان dtexec اجرا کند.3-1-4- Encrypt sensitive with password
به منظور رمزگذاری تنها مقادیر ویژگیهای حساس در Package از یک Password استفاده میشود. برای رمزگذاری از DPAPI استفاده میشود. دادههای حساس به عنوان بخشی از Package ذخیره میشوند اما آن دادهها با استفاده از Password رمزگذاری میشوند. به منظور باز نمودن Package در SSIS Designer کاربر باید رمز Package را ارائه دهد. اگر رمز ارائه نشود، Package بدون دادههای حساس باز میشود و کاربر باید مقادیر جدیدی برای دادههای حساس فراهم کند. اگر کاربر سعی نماید Package را بدون ارائه رمز اجرا کند، اجرای Package با خطا مواجه میشود.3-1-5- Encrypt sensitive with user key
به منظور رمزگذاری تنها مقادیر ویژگیهای حساس در Package از یک کلید که مبتنی بر Profile کاربر جاری میباشد، استفاده میشود. تنها کاربری که از همان Profile استفاده میکند، Package را میتواند بارگذاری (load) کند. اگر کاربر متفاوتی Package را باز نماید، اطلاعات حساس با مقادیر پوچی جایگزین میشود و کاربر باید مقادیر جدیدی برای دادههای حساس فراهم کند. اگر کاربر سعی نماید Package را بدون ارائه رمز اجرا کند، اجرای Package با خطا مواجه میشود. برای رمزگذاری از DPAPI استفاده میشود.3-1-6- (Rely on server storage for encryption (ServerStorage
با استفاده از نقشهای بانک اطلاعاتی، SQL Server تمامی Package را حفاظت میکند. این گزینه تنها زمانی پشتیبانی میشود که Package در بانک اطلاعاتی msdb ذخیره شده است.4- استفاده از نقشهای Integration Service
برای کنترل کردن دستیابی به Package، SSIS شامل 3 نقش ثابت در سطح بانک اطلاعاتی است. نقشها میتوانند تنها روی Package هایی که در بانک اطلاعاتی msdb ذخیره شده اند، بکار روند. با استفاده از SSMS میتوانید نقشها را به Packageها نسبت دهید، این انتساب نقشها در بانک اطلاعاتی msdb ذخیره میشود.Write action | Read action | Role |
Import packages Delete own packages Delete all packages Change own package roles Change all package roles * به نکته رجوع شود | Enumerate own packages Enumerate all packages View own packages View all packages Execute own packages Execute all packages Export own packages Export all packages Execute all packages in SQL Server Agent | db_ssisadmin or sysadmin |
Import packages Delete own packages Change own package roles | Enumerate own packages Enumerate all packages View own packages Execute own packages Export own packages | db_ssisltduser |
None | Enumerate all packages View all packages Execute all packages Export all packages Execute all packages in SQL Server Agent | db_ssisoperator |
Stop all currently running packages | View execution details of all running packages | Windows administrators |
همچنین جدول sysssispackages در بانک اطلاعاتی msdb شامل Package هایی است که در SQL Server ذخیره میشوند. این جدول شامل ستون هایی که اطلاعاتی درباره نقش هایی که به Packageها نسبت داده شده است، میباشد.
به صورت پیش فرض، مجوزهای نقشهای ثابت بانک اطلاعاتی db_ssisadmin و db_ssisoperator و شناسه منحصر به فرد کاربری (unique security identifier) که Package را ایجاد کرده برای خواندن Package بکار میرود، و مجوزهای نقش db_ssisadmin و شناسه منحصر به فرد کاربری که Package را ایجاد کرده برای نوشتن Package به کار میرود. یک User باید عضو نقش db_ssisadmin و db_ssisltduser یا db_ssisoperator برای داشتن دسترسی خواندن Package باشد. یک User باید عضو نقش db_ssisadmin برای داشتن دسترسی نوشتن Package باشد.
5- اتصال به صورت Remote به Integration Service
زمانی که یک کاربر بدون داشتن دسترسی کافی تلاش کند به یک Integration Service به صورت Remote متصل شود، با پیغام خطای "Access is denied" مواجه میشود. برای اجتناب از این پیغام خطا میتوان تضمین کرد که کاربر مجوز مورد نیاز DCOM را دارد.به منظور پیکربندی کردن دسترسی کاربر به صورت Remote به سرویس Integration مراحل زیر را دنبال کنید:
- Component Service را باز نمایید ( در Run عبارت dcomcnfg را تایپ کنید).مجوز دسترسی Lunch به منظور شروع و خاتمه سرویس، اعطا یا رد میشود و مجوز دسترسیActivation به منظور متصل شدن به سرویس، اعطا (grant) یا رد (deny) میشود.
- گره Component Service را باز کنید، گره Computer و سپس My Computer را باز نمایید و روی DCOM Config کلیک نمایید.
- گره DCOM Config را باز کنید و از لیست برنامه هایی که میتوانند پیکربندی شوند MsDtsServer را انتخاب کنید.
- روی Properties برنامه MsDtsServer رفته و قسمت Security را انتخاب کنید.
- در قسمت Lunch and Activation Permissions، مورد Customize را انتخاب و سپس روی Edit کلیک نمایید تا پنجره Lunch Permission باز شود.
- در پنجره Lunch Permission، کاربران را اضافه و یا حذف کنید و مجوزهای مناسب را به کاربران یا گروههای مناسب نسبت دهید. مجوزهای موجود عبارتند از Local Lunch، Remote Lunch، Local Activation و Remote Activation .
- در قسمت Access Permission مراحل فوق را به منظور نسبت دادن مجوزهای مناسب به کاربران یا گروههای مناسب انجام دهید.
- سرویس Integration را Restart کنید.
6- پیاده سازی
در ابتدا به ایجاد یک CLR Stored Procedures پرداخته میشود نام اسمبلی ساخته شده به این نام RunningPackage.dll میباشد و حاوی کد زیر است:Partial Public Class StoredProcedures '------------------------------------------------ 'exec dbo.Spc_NtDtexec 'Package','ssis','ssis@ssis','1234512345' '------------------------------------------------ <Microsoft.SqlServer.Server.SqlProcedure()> _ Public Shared Sub Spc_NtDtexec(ByVal PackageName As String, _ ByVal UserName As String, _ ByVal Password As String, _ ByVal Decrypt As String) Dim p As New System.Diagnostics.Process() p.StartInfo.FileName = "C:\Program Files\Microsoft SQL Server\100\DTS\Binn\DTExec.exe" p.StartInfo.RedirectStandardOutput = True p.StartInfo.Arguments = "/sql " & PackageName & " /User " & UserName & " /Password " & Password & " /De " & Decrypt p.StartInfo.UseShellExecute = False p.Start() p.WaitForExit() Dim output As String output = p.StandardOutput.ReadToEnd() Microsoft.SqlServer.Server.SqlContext.Pipe.Send(output) End Sub End Class
در قدم بعدی نیاز به Register کردن dll ساخته شده در سطح بانک اطلاعاتی SQL Server است، این گامها پس از اتصال به SQL Server Management Studio به شرح زیر است:
1- فعال کردن CLR در سرویس SQL Server
SP_CONFIGURE 'clr enabled',1 GO RECONFIGURE
2- فعال کردن ویژگی TRUSTWORTHY در بانک اطلاعاتی مورد نظر
ALTER DATABASE <Database Name> SET TRUSTWORTHY ON GO RECONFIGURE
3- ایجاد Assembly و Stored Procedure در بانک اطلاعاتی مورد نظر
Assembly ساخته شده با نام RunningPacakge.dll در ریشه :C کپی شود. بعد از ثبت نمودن این Assembly لزومی به وجود آن نمیباشد.
USE <Database Name> GO CREATE ASSEMBLY [RunningPackage] AUTHORIZATION [dbo] FROM 'C:\RunningPackage.dll' WITH PERMISSION_SET = UNSAFE Go CREATE PROCEDURE [dbo].[Spc_NtDtexec] @PackageName [nvarchar](50), @UserName [nvarchar](50), @Password [nvarchar](50), @Decrypt [nvarchar](50) WITH EXECUTE AS CALLER AS EXTERNAL NAME [RunningPackage].[RunningPackage.StoredProcedures].[Spc_NtDtexec] GO
در برنامه کاربردی تان کافی است متدی به شکل زیر ایجاد و با توجه به نیازتان در برنامه به فراخوانی آن و اجرای Package بپردازید.
Private Sub ExecutePackage() Dim oSqlConnection As SqlClient.SqlConnection Dim oSqlCommand As SqlClient.SqlCommand Dim strCnt As String = String.Empty strCnt = "Data Source=" & txtServer.Text & ";User ID=" & txtUsername.Text & ";Password=" & txtPassword.Text & ";Initial Catalog=" & cmbDatabaseName.SelectedValue.ToString() & ";" Try oSqlConnection = New SqlClient.SqlConnection(strCnt) oSqlCommand = New SqlClient.SqlCommand With oSqlCommand .Connection = oSqlConnection .CommandType = System.Data.CommandType.StoredProcedure .CommandText = "dbo.Spc_NtDtexec" .Parameters.Clear() .Parameters.Add("@PackageName", System.Data.SqlDbType.VarChar, 50) .Parameters.Add("@UserName", System.Data.SqlDbType.VarChar, 50) .Parameters.Add("@Password", System.Data.SqlDbType.VarChar, 50) .Parameters.Add("@Decrypt", System.Data.SqlDbType.VarChar, 50) .Parameters("@PackageName").Value = txtPackageName.Text.Trim() .Parameters("@UserName").Value = txtUsername.Text.Trim() .Parameters("@Password").Value = txtPassword.Text.Trim() .Parameters("@Decrypt").Value = txtDecrypt.Text.Trim() End With If (oSqlCommand.Connection.State <> System.Data.ConnectionState.Open) Then oSqlCommand.Connection.Open() oSqlCommand.ExecuteNonQuery() System.Windows.Forms.MessageBox.Show("Success") End If If (oSqlCommand.Connection.State = System.Data.ConnectionState.Open) Then oSqlCommand.Connection.Close() End If Catch ex As Exception MessageBox.Show(ex.Message, "Error") End Try End Sub 'ExecutePackage
مشخص سازی رشتههای متفاوت اتصالی
فرض کنید برنامهی جاری شما قرار است از دو بانک اطلاعاتی مشخص استفاده کند که تعاریف رشتههای اتصالی آنها در وب کانفیگ به صورت ذیل مشخص شدهاند:
<connectionStrings> <clear /> <add name="Sample07Context" connectionString="Data Source=(local);Initial Catalog=TestDbIoC;Integrated Security = true" providerName="System.Data.SqlClient" /> <add name="Database2012" connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true" providerName="System.Data.SqlClient" /> </connectionStrings>
تغییر Context برنامه جهت پذیرش رشتههای اتصالی پویای قابل تغییر در زمان اجرا
اکنون که قرار است کاربران در حین ورود به برنامه، بانک اطلاعاتی مدنظر خود را انتخاب کنند و یا سیستم قرار است به ازای کاربری خاص، رشتهی اتصالی خاص او را به Context ارسال کند، نیاز است Context برنامه را به صورت ذیل تغییر دهیم:
using System.Collections.Generic; using System.Data.Entity; using System.Linq; using EF_Sample07.DomainClasses; namespace EF_Sample07.DataLayer.Context { public class Sample07Context : DbContext, IUnitOfWork { public DbSet<Category> Categories { set; get; } public DbSet<Product> Products { set; get; } /// <summary> /// It looks for a connection string named Sample07Context in the web.config file. /// </summary> public Sample07Context() : base("Sample07Context") { } /// <summary> /// To change the connection string at runtime. See the SmObjectFactory class for more info. /// </summary> public Sample07Context(string connectionString) : base(connectionString) { //Note: defaultConnectionFactory in the web.config file should be set. } public void SetConnectionString(string connectionString) { this.Database.Connection.ConnectionString = connectionString; } } }
یک متد دیگر هم در اینجا در انتهای کلاس به نام SetConnectionString تعریف شدهاست. از این متد در حین ورود کاربر به سایت میتوان استفاده کرد. برای مثال حداقل دو نوع طراحی را میتوان درنظر گرفت:
الف) کاربر با برنامهای کار میکند که به ازای سالهای مختلف، بانکهای اطلاعاتی مختلفی دارد و در ابتدای ورود، یک drop down انتخاب سال کاری برای او درنظر گرفته شدهاست (علاوه بر سایر ورودیهای استانداردی مانند نام کاربری و کلمهی عبور). در این حالت بهتر است متد SetConnectionString نام رشتهی اتصالی را بر اساس سال انتخابی، در حین لاگین دریافت کند که اطلاعات آن در فایل کانفیگ سایت پیشتر مشخص شدهاست.
ب) کاربر یا مشتری پس از ورود به سایت، نیاز است صرفا از بانک اطلاعاتی خاص خودش استفاده کند. بنابراین اطلاعات تعریف کاربران و مشتریها در یک بانک اطلاعاتی مجزا قرار دارند و پس از لاگین، نیاز است رشتهی اتصالی او به صورت پویا از بانک اطلاعاتی خوانده شده و سپس توسط متد SetConnectionString تنظیم گردد.
مدیریت سشنهای رشتهی اتصالی جاری
پس از اینکه کاربر، در حین ورود مشخص کرد که از چه بانک اطلاعاتی قرار است استفاده کند و یا اینکه برنامه بر اساس اطلاعات ثبت شدهی او تصمیمگیری کرد که باید از کدام رشتهی اتصالی استفاده کند، نگهداری این رشتهی اتصالی نیاز به سشن دارد تا به ازای هر کاربر متصل به سایت منحصربفرد باشد. در مورد مدیریت سشنها در برنامههای وب، از نکات مطرح شدهی در مطلب «مدیریت سشنها در برنامههای وب به کمک تزریق وابستگیها» استفاده خواهیم کرد:
using System; using System.Threading; using System.Web; using EF_Sample07.DataLayer.Context; using EF_Sample07.ServiceLayer; using StructureMap; using StructureMap.Web; using StructureMap.Web.Pipeline; namespace EF_Sample07.IoCConfig { public static class SmObjectFactory { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static IContainer Container { get { return _containerBuilder.Value; } } public static void HttpContextDisposeAndClearAll() { HttpContextLifecycle.DisposeAndClearAll(); } private static Container defaultContainer() { return new Container(ioc => { // session manager setup ioc.For<ISessionProvider>().Use<DefaultWebSessionProvider>(); ioc.For<HttpSessionStateBase>() .Use(ctx => new HttpSessionStateWrapper(HttpContext.Current.Session)); ioc.For<IUnitOfWork>() .HybridHttpOrThreadLocalScoped() .Use<Sample07Context>() // Remove these 2 lines if you want to use a connection string named Sample07Context, defined in the web.config file. .Ctor<string>("connectionString") .Is(ctx => getCurrentConnectionString(ctx)); ioc.For<ICategoryService>().Use<EfCategoryService>(); ioc.For<IProductService>().Use<EfProductService>(); ioc.For<ICategoryService>().Use<EfCategoryService>(); ioc.For<IProductService>().Use<EfProductService>(); ioc.Policies.SetAllProperties(properties => { properties.OfType<IUnitOfWork>(); properties.OfType<ICategoryService>(); properties.OfType<IProductService>(); properties.OfType<ISessionProvider>(); }); }); } private static string getCurrentConnectionString(IContext ctx) { if (HttpContext.Current != null) { // this is a web application var sessionProvider = ctx.GetInstance<ISessionProvider>(); var connectionString = sessionProvider.Get<string>("CurrentConnectionString"); if (string.IsNullOrWhiteSpace(connectionString)) { // It's a default connectionString. connectionString = "Database2012"; // this session value should be set during the login phase sessionProvider.Store("CurrentConnectionStringName", connectionString); } return connectionString; } else { // this is a desktop application, so you can store this value in a global static variable. return "Database2012"; } } } }
نکتهی مهم این تنظیمات، قسمت مقدار دهی سازندهی کلاس Context برنامه به صورت پویا توسط IoC Container جاری است. در اینجا هر زمانیکه قرار است وهلهای از Sample07Context ساخته شود، از سازندهی دوم آن که دارای پارامتری به نام connectionString است، استفاده خواهد شد. همچنین مقدار آن به صورت پویا از متد getCurrentConnectionString که در انتهای کلاس تعریف شدهاست، دریافت میگردد.
در این متد ابتدا مقدار HttpContext.Current بررسی شدهاست. این مقدار اگر نال باشد، یعنی برنامهی جاری یک برنامهی دسکتاپ است و مدیریت رشتهی اتصالی جاری آنرا توسط یک خاصیت Static یا Singleton تعریف شدهی در برنامه نیز میتوان تامین کرد. از این جهت که در هر زمان، تنها یک کاربر در App Domain جاری برنامهی دسکتاپ میتواند وجود داشته باشد و Singleton یا Static تعریف شدن اطلاعات رشتهی اتصالی، مشکلی را ایجاد نمیکند. اما در برنامههای وب، چندین کاربر در یک App Domain به سیستم وارد میشوند. به همین جهت است که مشاهده میکنید در اینجا از تامین کنندهی سشن، برای نگهداری اطلاعات رشتهی اتصالی جاری کمک گرفته شدهاست.
کلید این سشن نیز در این مثال مساوی CurrentConnectionStringName تعریف شدهاست. بنابراین در حین لاگین موفقیت آمیز کاربر، دو مرحلهی زیر باید طی شوند:
sessionProvider.Store("CurrentConnectionString", "Sample07Context"); uow.SetConnectionString(WebConfigurationManager.ConnectionStrings[_sessionProvider.Get<string>("CurrentConnectionString")].ConnectionString);
سپس از متد SetConnectionString برای خواندن مقدار نام مشخص شده در سشن CurrentConnectionStringName کمک گرفتهایم. هرچند سازندهی کلاس Context برنامه، هر دو حالت استفاده از نام رشتهی اتصالی و یا مقدار کامل رشتهی اتصالی را پشتیبانی میکند، اما خاصیت this.Database.Connection.ConnectionString تنها رشتهی کامل اتصالی را میپذیرد (بکار رفته در متد SetConnectionString).
تا اینجا کار پویا سازی انتخاب و استفاده از رشتهی اتصالی برنامه به پایان میرسد. هر زمانیکه قرار است Context برنامه توسط IoC Container نمونه سازی شود، به متد getCurrentConnectionString رجوع کرده و مقدار رشتهی اتصالی را از سشن تنظیم شدهای به نام CurrentConnectionStringName دریافت میکند. سپس از مقدار آن جهت مقدار دهی سازندهی دوم کلاس Context استفاده خواهد کرد.
مدیریت migrations خودکار برنامه در حالت استفاده از چندین بانک اطلاعاتی
یکی از مشکلات کار با برنامههای چند دیتابیسی، به روز رسانی ساختار تمام بانکهای اطلاعاتی مورد استفاده، پس از تغییری در ساختار مدلهای برنامه است. از این جهت که اگر تمام بانکهای اطلاعاتی به روز نشوند، کوئریهای جدید برنامه که از خواص و فیلدهای جدید استفاده میکنند، دیگر کار نخواهند کرد. پویا سازی اعمال این تغییرات را میتوان به صورت ذیل انجام داد:
using System; using System.Data.Entity; using System.Web; using EF_Sample07.DataLayer.Context; using EF_Sample07.IoCConfig; namespace EF_Sample07.WebFormsAppSample { public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { initDatabases(); } private static void initDatabases() { // defined in web.config string[] connectionStringNames = { "Sample07Context", "Database2012" }; foreach (var connectionStringName in connectionStringNames) { Database.SetInitializer( new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>(connectionStringName)); using (var ctx = new Sample07Context(connectionStringName)) { ctx.Database.Initialize(force: true); } } } void Application_EndRequest(object sender, EventArgs e) { SmObjectFactory.HttpContextDisposeAndClearAll(); } } }
در این مثال خاص، متد initDatabases در حین آغاز برنامه فراخوانی شدهاست. منظور این است که اینکار در طول عمر برنامه تنها کافی است یکبار انجام شود و پس از آن است که EF Code first میتواند از رشتههای اتصالی متفاوتی که به آن ارسال میشود، بدون مشکل استفاده کند. زیرا اطلاعات نگاشت کلاسهای مدل برنامه به جداول بانک اطلاعاتی به این ترتیب است که کش میشوند و یا بر اساس کلاس Configuration به صورت خودکار به بانک اطلاعاتی اعمال میگردند.
کدهای کامل این مثال را که در حقیقت نمونهی بهبود یافتهی مطلب «EF Code First #12» است، از اینجا میتوانید دریافت کنید:
UoW-Sample
چک لیست تهیه یک برنامه ASP.NET MVC
روشهای تولید اعداد یا رشته تصادفی:
1- معمولترین روش تولید یک کد شش رقمی با استفاده از کلاس random
[TestMethod] public void TestRandomClass() { var code = new Random().Next(100000, 999999); Assert.IsTrue(code.ToString().Length == 6); }
[TestMethod] public void TestRandomWithEnumerable() { var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; var random = new Random(); var result = new string( Enumerable.Repeat(chars, 6) .Select(s => s[random.Next(s.Length)]) .ToArray()); Assert.IsTrue(result.Length == 6); }
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuwxyz~!@#$%^&*";
Guid.NewGuid().ToString().Replace("-", string.Empty).Substring(0, 6);
Guid.NewGuid().ToString("n").Substring(0, 6);
public static string GetUniqueKey(int maxSize) { char[] chars = new char[62]; chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray(); byte[] data = new byte[1]; using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) { crypto.GetNonZeroBytes(data); data = new byte[maxSize]; crypto.GetNonZeroBytes(data); } StringBuilder result = new StringBuilder(maxSize); foreach (byte b in data) { result.Append(chars[b % (chars.Length)]); } return result.ToString(); } }
5 - استفاده از متد Path.GetRandomFileName
public string Get8CharacterRandomString() { string path = Path.GetRandomFileName(); path = path.Replace(".", ""); // Remove period. return path.Substring(0, 6); // Return 6 character string }
var chars = "abcdefghijklmnopqrstuvwxyz123456789".ToArray(); string pw = Enumerable.Range(0, passwordLength) .Aggregate( new StringBuilder(), (sb, n) => sb.Append((chars[random.Next(chars.Length)])), sb => sb.ToString());
تزریق وابستگیها
- برای آزمایش این مساله یک break point را در سازنده کلاسهای مورد استفاده قرار دهید.
- Application partitioning: به طور کلی جهت دستیابی به اطلاعات (از محیط عملیاتی و یا محیطهای گزارشی گیری با ماهیت آرشیو) از درون خود برنامه کاربردی با ماشین مورد نظر ارتباط برقرار میگردید.
-(Partition Views (DPVs: جداول یکسانی ایجاد میگردید، که هر یک حاوی قسمتی از اطلاعات بودند و بدین ترتیب برای دستیابی به تمامی دادههای این جداول با ایجاد یک View (پیوند میان این جداول) کلیه اطلاعات قابل دسترسی بود.
در تکنیک پارتیشنینگ کلیه محتوای جدول بصورت یکجا قابل دستیابی است، همچنین جهت آرشیو با استفاده از عملگر Switch این امکان را داریم که در کسری از ثانیه (با تنها انتقال Meta data) به آرشیو اطلاعات بپردازیم.
تا پیش از انتشار نسخه 2012 توان پشتیبانی از 1.000 پارتیشن وجود داشت این امکان در نسخه 2014 به 15.000 پارتیشن رسیده است.
همانگونه که ذکر گردید جهت آرشیو از عملگر Switch استفاده میشود (و نه عملگر Merge) همچنین در خصوص نحوه انجام پارتیشن بندی از آنجا که تابع یک Data type میگیرد الزامی به نوع Date نیست و مطابق شکل میتواند نوع آن Int نیز باشد، ولی عملیات آرشیو در ذات خود به اطلاعات تاریخچه ای اشاره میکند. جهت اطلاعات بیشتر به لینک Partitioned Tables and Indexes مراجعه شود.
Generic Host چیست؟
Generic Host یکی از ویژگیهای جدید ASP.NET Core 2.1 است. هدف آن جداسازی HTTP pipeline برنامه، از Web Host API آن است. یکی از مزایای اینکار، امکان استفادهی از آن نه فقط در پروژههای وب، بلکه در پروژههای کنسول نیز میباشد. به این ترتیب میتوان کارهای غیر HTTP را از برنامهی وب مجزا کرد تا به کارآیی بیشتری رسید و برای این منظور اینترفیس IHostedService را که در فضای نام Microsoft.Extensions.Hosting قرار دارد، برای ثبت کارهای پسزمینهی خارج از اعمال web host جاری، ارائه دادهاند:
namespace Microsoft.Extensions.Hosting { public interface IHostedService { Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); } }
یک مثال: معرفی کار پسزمینهای که هر دو ثانیه یکبار انجام میشود
در SampleHostedService زیر، عبارت Hosted service executing به همراه زمان جاری، هر دو ثانیه یکبار لاگ میشود و اگر برنامه را توسط دستور dotnet run اجرا کنید، میتوانید خروجی آنرا در کنسول، مشاهده کنید:
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace MvcTest { public class SampleHostedService : IHostedService { private readonly ILogger<SampleHostedService> _logger; public SampleHostedService(ILogger<SampleHostedService> logger) { _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting Hosted service"); while (!cancellationToken.IsCancellationRequested) { _logger.LogInformation("Hosted service executing - {0}", DateTime.Now); await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); } } public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Stopping Hosted service"); return Task.CompletedTask; } } }
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IHostedService, SampleHostedService>();
services.AddHostedService<SampleHostedService>();
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
Startup.cs(82,56): error CS0104: 'IHostingEnvironment' is an ambiguous reference between 'Microsoft.AspNetCore.Hosting.IHostingEnvironment' and 'Microsoft.Extensions.Hosting.IHostingEnvironment'
مشکلات پیاده سازی کار پسزمینهی SampleHostedService فوق
هر چند اگر مثال فوق را اجرا کنید، خروجی مناسبی را دریافت خواهید کرد، اما دارای این اشکال مهم نیز هست:
D:\MvcTest>dotnet run info: MvcTest.SampleHostedService[0] Starting Hosted service info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 14:45:10 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 14:45:12 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 14:45:14 Ctrl+C Application is shutting down... Hosting environment: Development Content root path: D:\MvcTest Now listening on: https://localhost:5001 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down.
از دیدگاه ASP.NET Core، یک کار پس زمینه زمانی خاتمه یافته محسوب میشود که متد StartAsync، مقدار Task.CompletedTask را بازگرداند؛ در غیراینصورت، در حال اجرا درنظر گرفته میشود و چون در پیاده سازی فوق این نکته رعایت نشدهاست، این Task همواره در حال اجرا و خاتمه نیافته محسوب میشود و نوبت به مابقی کارها نخواهد رسید. همچنین در قسمت StopAsync نیز بهتر است یک فیلد CancellationTokenSource تعریف شدهی در سطح کلاس را مورد استفاده قرار داد و متد Cancel آنرا فراخوانی کرد تا اطلاع رسانی صحیحی را به متد StartAsync در مورد خاتمهی برنامه، انجام دهد.
برای این منظور و جهت ساده سازی و پیاده سازی تمام این نکات، از اینترفیس خام IHostedService، یک کلاس abstract به نام BackgroundService نیز در فضای نام Microsoft.Extensions.Hosting پیش بینی شدهاست:
namespace Microsoft.Extensions.Hosting { public abstract class BackgroundService : IHostedService, IDisposable { protected BackgroundService(); public virtual void Dispose(); public virtual Task StartAsync(CancellationToken cancellationToken); public virtual Task StopAsync(CancellationToken cancellationToken); protected abstract Task ExecuteAsync(CancellationToken stoppingToken); } }
namespace MvcTest { public class PrinterHostedService : BackgroundService { private readonly ILogger<SampleHostedService> _logger; public PrinterHostedService(ILogger<SampleHostedService> logger) { _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Starting Hosted service"); while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Hosted service executing - {0}", DateTime.Now); await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); } } } }
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHostedService<PrinterHostedService>();
D:\MvcTest>dotnet run Hosting environment: Development infoContent root path: D:\MvcTest Now listening on: https://localhost:5001 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down. : MvcTest.SampleHostedService[0] Starting Hosted service info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 15:00:23 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 15:00:25 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 15:00:27 Application is shutting down... ^C
یک نکته: تزریق وابستگی DbContext برنامه در یک سرویس کار پسزمینه
IHostedServiceها با طول عمر singleton به سیستم تزریق وابستگیها معرفی میشوند. در این حالت اگر سرویسهایی با طول عمر transient و یا scoped را به آنها تزریق کنید، دیگر طول عمر مدنظر شما را نداشته و آنها هم به صورت singleton عمل خواهند کرد. هر چند خود سیستم تزریق وابستگیهای NET Core. با صدور استثنائی، از این مساله جلوگیری میکند (در این مورد در مطالب «مهارتهای تزریق وابستگیها در برنامههای NET Core. - قسمت چهارم - پرهیز از الگوی Service Locator در برنامههای وب» و همچنین «قسمت سوم - رهاسازی منابع سرویسهای IDisposable» بیشتر بحث شدهاست). یک چنین مواردی را به صورت زیر با تزریق IServiceScopeFactory و ساخت صریح یک Scope میتوان مدیریت کرد:
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; public abstract class ScopedBackgroundService : BackgroundService { private readonly IServiceScopeFactory _serviceScopeFactory; public ScopedBackgroundService(IServiceScopeFactory serviceScopeFactory) { _serviceScopeFactory = serviceScopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using (var scope = _serviceScopeFactory.CreateScope()) { await ExecuteInScope(scope.ServiceProvider, stoppingToken); } } public abstract Task ExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken); }
طراحی سرویس کارهای پسزمینهی زمانبندی شده
ASP.NET Core، متد ExecuteAsync را یکبار بیشتر اجرا نمیکند. بنابراین پیاده سازی تایمری که بخواهد برای مثال ارسال ایمیلهای خبرنامهی سایت را هر روز ساعت 11 شب انجام دهد، به خود ما واگذار شدهاست. برای پیاده سازی بهتر این تایمر میتوان از کتابخانهی NCrontab که توسط نویسندهی کتابخانهی معروف ELMAH تهیه شدهاست، استفاده کرد که با برنامههای NET Core. نیز سازگاری دارد:
dotnet add package ncrontab
┌───────────── minute (0 - 59) │ ┌───────────── hour (0 - 23) │ │ ┌───────────── day of month (1 - 31) │ │ │ ┌───────────── month (1 - 12) │ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday; │ │ │ │ │ 7 is also Sunday on some systems) │ │ │ │ │ │ │ │ │ │ * * * * *
* * * * * * - - - - - - | | | | | | | | | | | +--- day of week (0 - 6) (Sunday=0) | | | | +----- month (1 - 12) | | | +------- day of month (1 - 31) | | +--------- hour (0 - 23) | +----------- min (0 - 59) +------------- sec (0 - 59)
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using NCrontab; using static NCrontab.CrontabSchedule; public abstract class ScheduledScopedBackgroundService : ScopedBackgroundService { private CrontabSchedule _schedule; private DateTime _nextRun; protected abstract string Schedule { get; } public ScheduledScopedBackgroundService(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) { _schedule = CrontabSchedule.Parse(Schedule, new ParseOptions { IncludingSeconds = true }); _nextRun = _schedule.GetNextOccurrence(DateTime.Now); } public override async Task ExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken) { do { var now = DateTime.Now; if (now > _nextRun) { await ScheduledExecuteInScope(serviceProvider, stoppingToken); _nextRun = _schedule.GetNextOccurrence(DateTime.Now); } await Task.Delay(1000, stoppingToken); //1 second delay } while (!stoppingToken.IsCancellationRequested); } public abstract Task ScheduledExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken); }
روش استفادهی از آن برای تعریف یک وظیفهی جدید نیز به صورت زیر است:
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; public class MyScheduledTask : ScheduledScopedBackgroundService { private readonly ILogger<MyScheduledTask> _logger; public MyScheduledTask( IServiceScopeFactory serviceScopeFactory, ILogger<MyScheduledTask> logger) : base(serviceScopeFactory) { _logger = logger; } protected override string Schedule => "*/10 * * * * *"; //Runs every 10 seconds public override Task ScheduledExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken) { _logger.LogInformation("MyScheduledTask executing - {0}", DateTime.Now); return Task.CompletedTask; } }
روش معرفی آن به سیستم نیز مانند قبل است:
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHostedService<MyScheduledTask>();
D:\MvcTest>dotnet run Hosting environment: Development Content root path: D:\MvcTest Now listening on: https://localhost:5001 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down. info: MyScheduledTask[0] MyScheduledTask executing - 02/19/2019 19:18:50 info: MyScheduledTask[0] MyScheduledTask executing - 02/19/2019 19:19:00 info: MyScheduledTask[0] MyScheduledTask executing - 02/19/2019 19:19:10 Application is shutting down... ^C