Chào các bạn, Để đổi không khí hôm nay mình sẽ viết một bài viết hướng xây dựng một ứng dụng console application bằng net core chạy như một windows service. Chúng ta có rất nhiều ứng dụng bằng console application để làm một việc như lắng nghe queue và thực thi một số công việc gì đó. Ở .net 4.6 chúng ta chỉ cần kế thừa những class từ ServiceBase class là có thể làm được. Sau đó chỉ cần sử dụng sc.exe để install như là một windows services. Nhưng không may mắn là ở .net core chúng ta không có ServiceBase và những third-party như Topshelf cũng chưa hỗ trợ .net core (đang phát triển trên repo của họ những chưa release ). Đang lúc mình cần làm và sẵn tiện viết một bài hướng dẫn cho các bạn luôn.
Host ASP.Net core as a windows service
Dự án hiện tại của đang mình phát triển bằng .net core cũng gần một năm. Mọi thứ tính đến bây giờ thì vẫn ok. Dự án của mình bắt đầu ở phiên bản 2.0. Hiện tại thì vẫn 2.0 nhưng lười upgrade lên 2.1 =)) Hệ thống bắt đầu phát triển lớn ra và cần những service chạy bên ngoài như là xuất PDF, gửi mail, ghi log…v..v… Giải pháp lúc đầu của mình là vẫn xây dựng những webservice và deploy trên IIS. Những để đảm bảo những service đó luôn sống để lắng nghe các event thì phải set app pool luôn chạy. Nghe có vẻ rất tốn kém nhỉ. Mình bắt đầu thay đổi giải pháp là chuyển những webservice thành windows service nhưng .net core không hỗ trợ ServiceBase. ServiceBase cung cấp một life cycle cho ứng dụng console application để quản lý các state như start, pause, stop.
Và bắt đầu tìm kiếm Google với từ khóa “Host .net core as a windows service” và kết quả đầu tiên đến từ tài liệu của Microsoft. Hướng dẫn host ứng dụng asp.net core như một windows service. Chỉ cần vài bước đơn giản theo hướng dẫn đó là có thể tạo một ứng dụng và install như một windows service. Chúng ta có thể hài lòng với kết quả đó.
public static void Main(string[] args) { var isService = !(Debugger.IsAttached || args.Contains("--console")); var builder = CreateWebHostBuilder(args.Where(arg => arg != "--console").ToArray()); if (isService) { var pathToExe = Process.GetCurrentProcess().MainModule.FileName; var pathToContentRoot = Path.GetDirectoryName(pathToExe); builder.UseContentRoot(pathToContentRoot); } var host = builder.Build(); if (isService) { host.RunAsService(); } else { host.Run(); } } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, config) => { // Configure the app here. }) .UseStartup<Startup>();
Nhưng xem xét kỹ chúng ta sẽ thấy ứng dụng sẽ tạo một WebHost để chạy. WebHost sẽ có nhiều thành phần cấu thành nó và sẽ được khởi tạo khi WebHost chạy. Nhưng thực tế chúng ta không cần đến chúng. Thế là lại phải tìm một cách khác. Lướt thêm vài trang và mình đã tìm ra được giải pháp. Giải pháp đó là ví dụ mình sẽ làm bên dưới.
Tạo ứng dụng console bằng .net core
Điều đầu tiên là tạo một ứng dụng .net core. Bạn có thể tạo bằng dotnet-cli hay visual studio gì cũng được. Sau đó sửa file project .csproj như sau
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp2.1</TargetFramework> <OutputType>Exe</OutputType> <RuntimeIdentifiers>win10-x64</RuntimeIdentifiers> <LangVersion>7.1</LangVersion> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.1.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.1.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" /> <PackageReference Include="System.ServiceProcess.ServiceController" Version="4.5.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="2.1.1" /> </ItemGroup> <ItemGroup> <None Update="appsettings.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup> </Project>
Mình sẽ giải thích một số thẻ: đầu tiên là TargetFramework hiện tại của mình là 2.1. OutputType là file exe. RuntimeIdentifiers là target platform mình sẽ sử dụng app này, của mình ở đây là win 10 64bit. LangVersion là phiên bản cho ngôn ngữ c#, ở đây mình set là 7.1 để sử dụng được async cho hàm Main và một số package kèm theo cuối cùng là include file appsettings.json để mỗi lần build sẽ copy quăng vào thư mục chạy. đơn giản vậy thôi.
Project Files
Tiếp đến chúng ta sẽ cài đặt một số class. Cấu trúc project sẽ như sau.
Startup
Đầu tiên là một interface IStartup và một implement Startup của interface đó. Nhiệm vụ của nó chỉ là khai báo các service. Ở đây chúng ta không cần config request pipeline như một ứng dụng Asp.
public interface IStartup { IServiceCollection ConfigureServices(IServiceCollection services); }
public class Startup : IStartup { public IServiceCollection ConfigureServices(IServiceCollection services) { // Đăng ký các service ở đây services.AddScoped<..,..>(); return services; } }
ServiceBaseLifetime
public class ServiceBaseLifetime : ServiceBase, IHostLifetime { private IApplicationLifetime ApplicationLifetime { get; } private readonly TaskCompletionSource<object> _delayStart = new TaskCompletionSource<object>(); public ServiceBaseLifetime(IApplicationLifetime applicationLifetime) { ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime)); } public Task WaitForStartAsync(CancellationToken cancellationToken) { cancellationToken.Register(() => _delayStart.TrySetCanceled()); ApplicationLifetime.ApplicationStopping.Register(Stop); new Thread(Run).Start(); // Otherwise this would block and prevent IHost.StartAsync from finishing. return _delayStart.Task; } public Task StopAsync(CancellationToken cancellationToken) { Stop(); return Task.CompletedTask; } // Called by base.Run when the service is ready to start. protected override void OnStart(string[] args) { _delayStart.TrySetResult(null); base.OnStart(args); } // Called by base.Stop. This may be called multiple times by service Stop, ApplicationStopping, and StopAsync. // That's OK because StopApplication uses a CancellationTokenSource and prevents any recursion. protected override void OnStop() { ApplicationLifetime.StopApplication(); base.OnStop(); } private void Run() { try { Run(this); // This blocks until the service is stopped. _delayStart.TrySetException(new InvalidOperationException("Stopped without starting")); } catch (Exception ex) { _delayStart.TrySetException(ex); } } }
Cái này copy từ Microsoft. Đây là class kế thừa ServiceBase dùng để tạo life cycle cho ứng dụng windows service. Nó cũng kế thừa IHostLifetime từ package Microsoft.Extensions.Hosting.
ServiceBaseLifetimeHostExtensions
public static class ServiceBaseLifetimeHostExtensions { public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder) { return hostBuilder .ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceBaseLifetime>()); } public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default) { return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken); } public static IHostBuilder UseAppsetting(this IHostBuilder hostBuilder) { return hostBuilder.ConfigureAppConfiguration(((hostContext, configurationBuilder) => { var env = hostContext.HostingEnvironment; configurationBuilder .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); configurationBuilder.AddEnvironmentVariables(); var configuration = configurationBuilder.Build(); })); } public static IHostBuilder UseStartup<TStartup>(this IHostBuilder hostBuilder) where TStartup : class { return hostBuilder.UseStartup(typeof(TStartup)); } public static IHostBuilder UseStartup(this IHostBuilder hostBuilder, Type startupType) { return hostBuilder.ConfigureServices((Action<IServiceCollection>)(services => { if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo())) { var startupInstance = Activator.CreateInstance(startupType) as IStartup; services = startupInstance.ConfigureServices(services); } })); } }
Đây là class chứa một số extension như Run host, config appsettings.json và sử dụng class startup. Code tự soi mình sẽ không đi sâu vào 🙂
Program
class Program { static async Task Main(string[] args) { var isService = !(Debugger.IsAttached || args.Contains("--console")); var builder = new HostBuilder() .UseAppsetting() .UseStartup<Startup>(); if (isService) { await builder.RunAsServiceAsync(); } else { await builder.RunConsoleAsync(); } } }
Cái class này quá quen thộc với một ứng dụng console. Phương thức main sẽ tạo Host và config một số thứ. nếu là chế độ debug hoặc console thì sẽ chạy dưới dạng console. ngược lại sẽ chạy dưới dạng service.
FileWriterService
public class FileWriterService : IHostedService, IDisposable { private readonly ITestService _testService; private const string Path = @"d:\TestApplication.txt"; private Timer _timer; public FileWriterService(ITestService testService) { _testService = testService; } public Task StartAsync(CancellationToken cancellationToken) { _testService.TestMethod(); WriteTimeToFile("Start Application"); _timer = new Timer( (e) => WriteTimeToFile(), null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { WriteTimeToFile("Stop Application"); _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } public void WriteTimeToFile(string text = null) { text = text ?? DateTime.UtcNow.ToString("O"); if (!File.Exists(Path)) { using (var sw = File.CreateText(Path)) { sw.WriteLine(text); } } else { using (var sw = File.AppendText(Path)) { sw.WriteLine(text); } } } public void Dispose() { _timer?.Dispose(); } }
là một service kế thừa từ IHostedService. class này sẽ được start hoặc stop bởi Host một khi chúng ta đăng ký nó trong Startup. Nó chỉ đơn giản là khỏi tạo một Timer và ghi lại thời gian mỗi phút. Mục đích để chúng ta biết được nó đang chạy =)) Không có gì phức tạp cả. Đừng quên đăng ký nó vào Startup bằng dòng sau services.AddHostedService<FileWriterService>();
. Mọi thứ có vẻ Ok rồi đó. Build lại và fix nếu nó có lỗi.
Publish project
Việc tiếp theo là chúng ta sẽ publish nó thành file exe để install vào windows serivce. Bạn muốn publish bằng visual studio hay dotnet-cli đều được
Bằng visual studio: click phải trên project chọn Publish và config như sau
Bằng dotnet-cli:
dotnet publish -c Release -o D:\WindowsService -r win10-x64
Create windows service
Bước cuối cùng, chúng ta sẽ đăng ký như một windows service bằng command sau:
sc create MyFileService binPath= "D:\WindowsService\WindowsServiceSample.exe"
(cần chạy command prompt dưới quyền Administrator)
Sau đó chay service bằng command:
sc start MyFileService
Như vậy là service của bạn đã được chạy để kiểm tra bằng cách vào thư mục D:\TestApplication.txt (mình hardcode ở class FileWriterService). Như vậy là chúng ta đã tạo thành công một ứng dụng .net core chạy như một windows service. Thật dễ phải không? :v
Kết luận
Như vậy chúng ta đã tạo được một windows serivce đơn giản bằng .net core bằng những dòng code hết sức là đơn giản. Hy vọng nhiêu đây đã đủ cho các bạn bắt đầu để xây dụng một ứng dụng windows service. Hy vọng trong thời gian tới thì Microsoft sẽ cập nhật thêm những tính năng này. Topshelf là một trong những third-party hỗ trợ xây dựng ứng dụng windows service và họ đang phát triển cho .net core và sẽ release trong thời gian tới. Nếu bạn có biết một cái nào đó hay hơn và hỗ trợ .net core thì hãy nói cho mình biết bằng cách comment bên dưới nhé. 🙂
Xem thêm bài viết về .Net tại đây
Tham khảo source code trên Github