├── .gitignore ├── ApiService ├── Example.ApiService.fsproj ├── Program.fs ├── Worker.fs └── appsettings.json ├── ConsoleService ├── Example.ConsoleService.fsproj ├── Logger.fs └── Program.fs ├── HostedService ├── BackgroundWorker.fs ├── BrokenWorker.fs ├── Example.HostedService.fsproj ├── ExceptionWorker.fs ├── HostedWorker.fs ├── Program.fs └── appsettings.json ├── LICENSE ├── Library ├── Example.Library.fsproj ├── Task.fs └── Timer.fs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # .NET build artifacts 2 | bin/ 3 | obj/ 4 | 5 | # VS Code user settings 6 | .vscode -------------------------------------------------------------------------------- /ApiService/Example.ApiService.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ApiService/Program.fs: -------------------------------------------------------------------------------- 1 | module Example.ApiService.Program 2 | 3 | open System 4 | open Microsoft.AspNetCore.Builder 5 | open Microsoft.Extensions.DependencyInjection 6 | open Microsoft.Extensions.Hosting 7 | 8 | let args = Environment.GetCommandLineArgs()[1..] 9 | let builder = WebApplication.CreateBuilder(args) 10 | builder.Services.AddHostedService() |> ignore 11 | 12 | let application = builder.Build() 13 | application.MapGet("/", Func((fun () -> "Hello World!"))) |> ignore 14 | application.Run() -------------------------------------------------------------------------------- /ApiService/Worker.fs: -------------------------------------------------------------------------------- 1 | namespace Example.ApiService 2 | 3 | open System 4 | open System.Threading 5 | open Example.Library 6 | open Microsoft.Extensions.Hosting 7 | open Microsoft.Extensions.Logging 8 | 9 | type Worker(logger: ILogger) = 10 | inherit BackgroundService() 11 | 12 | override _.ExecuteAsync(token: CancellationToken) = 13 | task { 14 | logger.LogInformation "Launching 🚀" 15 | 16 | while not token.IsCancellationRequested do 17 | logger.LogInformation $"Running {DateTime.Now:T}" 18 | do! Task.delay (TimeSpan.FromSeconds 1) token 19 | 20 | logger.LogInformation "Quitting 👋" 21 | } 22 | -------------------------------------------------------------------------------- /ApiService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /ConsoleService/Example.ConsoleService.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net7.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ConsoleService/Logger.fs: -------------------------------------------------------------------------------- 1 | module Example.ConsoleService.Logger 2 | 3 | open Microsoft.Extensions.Logging 4 | 5 | type private Says = class end 6 | 7 | let create () = 8 | LoggerFactory 9 | .Create(fun builder -> builder.AddConsole() |> ignore) 10 | .CreateLogger() 11 | :> ILogger -------------------------------------------------------------------------------- /ConsoleService/Program.fs: -------------------------------------------------------------------------------- 1 | module Example.ConsoleService.Program 2 | 3 | open System 4 | open Example.Library 5 | open Microsoft.Extensions.Logging 6 | 7 | let logger = Logger.create() 8 | logger.LogInformation "Launching 🚀" 9 | 10 | let timer = Timer.everySecond (fun now -> logger.LogInformation $"Service Running {now:T}") 11 | Console.ReadKey() |> ignore 12 | 13 | logger.LogInformation "Cleaning Up 🧹" 14 | timer.Dispose() 15 | 16 | logger.LogInformation "Quitting 👋" 17 | -------------------------------------------------------------------------------- /HostedService/BackgroundWorker.fs: -------------------------------------------------------------------------------- 1 | namespace Example.HostedService 2 | 3 | open System.Threading 4 | open Example.Library 5 | open Microsoft.Extensions.Hosting 6 | open Microsoft.Extensions.Logging 7 | 8 | type BackgroundWorker(logger: ILogger) = 9 | inherit BackgroundService() 10 | 11 | override _.ExecuteAsync(token: CancellationToken) = 12 | task { 13 | logger.LogInformation "Launching 🚀" 14 | 15 | let timer = Timer.everySecond (fun now -> logger.LogInformation $"Running {now:T}") 16 | do! Task.wait token 17 | 18 | logger.LogInformation "Cleaning Up 🧹" 19 | timer.Dispose() 20 | 21 | logger.LogInformation "Quitting 👋" 22 | } 23 | -------------------------------------------------------------------------------- /HostedService/BrokenWorker.fs: -------------------------------------------------------------------------------- 1 | namespace Example.HostedService 2 | 3 | open System 4 | open System.Threading 5 | open System.Threading.Tasks 6 | open Example.Library 7 | open Microsoft.Extensions.Hosting 8 | open Microsoft.Extensions.Logging 9 | 10 | type BrokenWorker(logger: ILogger) = 11 | inherit BackgroundService() 12 | 13 | override _.ExecuteAsync(token: CancellationToken) = 14 | logger.LogInformation "Launching 🚫" 15 | 16 | while not token.IsCancellationRequested do 17 | logger.LogInformation $"Running {DateTime.Now:T}" 18 | Thread.Sleep 1000 19 | 20 | Task.CompletedTask 21 | -------------------------------------------------------------------------------- /HostedService/Example.HostedService.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /HostedService/ExceptionWorker.fs: -------------------------------------------------------------------------------- 1 | namespace Example.HostedService 2 | 3 | open System 4 | open System.Threading 5 | open Example.Library 6 | open Microsoft.Extensions.Hosting 7 | open Microsoft.Extensions.Logging 8 | 9 | type ExceptionWorker(logger: ILogger) = 10 | inherit BackgroundService() 11 | 12 | let random = new Random() 13 | 14 | override _.ExecuteAsync(token: CancellationToken) = 15 | task { 16 | logger.LogInformation "Launching 💣" 17 | 18 | while not token.IsCancellationRequested do 19 | if random.NextDouble() >= 0.7 20 | then 21 | logger.LogInformation $"Exception 💥" 22 | failwith "Oh no!" 23 | else 24 | logger.LogInformation $"Continuing" 25 | do! Task.delay (TimeSpan.FromSeconds 1) token 26 | } 27 | -------------------------------------------------------------------------------- /HostedService/HostedWorker.fs: -------------------------------------------------------------------------------- 1 | namespace Example.HostedService 2 | 3 | open System 4 | open System.Threading 5 | open Example.Library 6 | open Microsoft.Extensions.Hosting 7 | open Microsoft.Extensions.Logging 8 | 9 | type HostedWorker(logger: ILogger) = 10 | let timer = Timer.create (fun now -> logger.LogInformation $"Running {now:T}") 11 | 12 | interface IHostedService with 13 | member _.StartAsync(token: CancellationToken) = 14 | task { 15 | logger.LogInformation "Launching 🚀" 16 | do! Timer.start (TimeSpan.FromSeconds 1) token timer 17 | } 18 | 19 | member _.StopAsync(token: CancellationToken) = 20 | task { 21 | do! Timer.stop token timer 22 | logger.LogInformation "Quitting 👋" 23 | } 24 | 25 | interface IDisposable with 26 | member _.Dispose() = 27 | logger.LogInformation "Cleaning Up 🧹" 28 | timer.Dispose() 29 | -------------------------------------------------------------------------------- /HostedService/Program.fs: -------------------------------------------------------------------------------- 1 | module Example.HostedService.Program 2 | 3 | open System 4 | open Microsoft.Extensions.DependencyInjection 5 | open Microsoft.Extensions.Hosting 6 | 7 | let args = Environment.GetCommandLineArgs().[1..] 8 | Host.CreateDefaultBuilder(args) 9 | .ConfigureServices(fun _ services -> 10 | services 11 | // .AddHostedService() 12 | .AddHostedService() 13 | // .AddHostedService() 14 | // .AddHostedService() 15 | |> ignore) 16 | .Build() 17 | .Run() -------------------------------------------------------------------------------- /HostedService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Teodor Elstad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Library/Example.Library.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Library/Task.fs: -------------------------------------------------------------------------------- 1 | module Example.Library.Task 2 | 3 | open System 4 | open System.Threading 5 | open System.Threading.Tasks 6 | 7 | let delay (delay: TimeSpan) (token: CancellationToken) = 8 | task { 9 | try 10 | do! Task.Delay(delay, token) 11 | with 12 | | :? TaskCanceledException -> return () 13 | | :? ObjectDisposedException -> return () 14 | | error -> raise error 15 | } 16 | 17 | let wait = delay Timeout.InfiniteTimeSpan -------------------------------------------------------------------------------- /Library/Timer.fs: -------------------------------------------------------------------------------- 1 | module Example.Library.Timer 2 | 3 | open System 4 | open System.Threading 5 | open System.Threading.Tasks 6 | 7 | let create (run: DateTime -> unit) = 8 | new Timer((fun _ -> run DateTime.Now), 0, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan) 9 | 10 | let start (period: TimeSpan) (token: CancellationToken) (timer: Timer) = 11 | if token.IsCancellationRequested 12 | then Task.CompletedTask 13 | else 14 | timer.Change(TimeSpan.Zero, period) |> ignore 15 | Task.CompletedTask 16 | 17 | let stop (token: CancellationToken) (timer: Timer) = 18 | if token.IsCancellationRequested 19 | then Task.CompletedTask 20 | else 21 | timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan) |> ignore 22 | Task.CompletedTask 23 | 24 | let everySecond (run: DateTime -> unit) = 25 | new Timer((fun _ -> run DateTime.Now), 0, TimeSpan.Zero, TimeSpan.FromSeconds 1) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | .NET Generic Host with F# 2 | ========================= 3 | _Are you tired of handling graceful shutdown manually by listening for key-events? Are you held awake at night by doubts of mishandled exceptions and restarts? Then this is the repository for you!_ 4 | 5 | Why should I care about Generic Host? 6 | ------------------------------------- 7 | From time to time we write programs that acts as services, without being HTTP-APIs. Maybe we're reading messages from a queue, streaming events from a pipe or just simply keeping an open connection to something, either way we want a service that stays up when we want it, and shuts down cleanly. 8 | 9 | It might be tempting to write such services as simple console applications, and just stick a `while true do` or `Console.ReadKey()` in somewhere to keep it running. Sometimes this is sufficient, but is this always the best option? 10 | 11 | .NET Core 2.1 introduced [.NET Generic Host](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host), and in this repository we'll have a look at some of the ways you can use Generic Host to create more robust services. 12 | 13 | ### Doesn't Kubernetes do all that work for me? 14 | Kubernetes can do a lot of heavy lifting when it comes to [restarting failing programs](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/) and keeping long [lived programs running](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/), but it still really want you to [handle termination of your program](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination) in a clean and efficient manner. 15 | 16 | ### You won me over! How do I go about navigating this repository? 17 | _TODO: Currently all you have is the talk outline._ 18 | 19 | Talk outline 20 | ------------ 21 | __ConsoleService__ 22 | * Run & Any Key. Works! Ship it! 23 | * Run & CTRL + C. Wait, what about Stop and Dispose? 24 | * SIGTERM. Whoops, it's broken :-/ 25 | 26 | __HostedService__ 27 | * Run & CTRL + C. Works! 28 | * SIGTERM. Works! 29 | * BrokenWorker + BackgroundWorker. Blocks startup. 30 | * HostedWorker. Better fit for some applications. 31 | * ExceptionWorker + HostedWorker. Unhandled exceptions shuts down host. 32 | 33 | __ApiService__ 34 | * You can use `IHostedService` and `BackgroundService` with ASP.NET Core. 35 | * Remember that you'll have to handle scopes yourself. 36 | 37 | __More info__ 38 | * Check out [.NET Generic Host](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host) 39 | * Check out [Fundamentals: hosted services](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services) 40 | --------------------------------------------------------------------------------