├── .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 |
--------------------------------------------------------------------------------