• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Aldaviva / Unfucked / 19430876270

17 Nov 2025 01:13PM UTC coverage: 47.301% (-0.5%) from 47.79%
19430876270

push

github

Aldaviva
DI: fix super registration with hosted services where another hosted service also depends on this hosted service, which would cause an infinite loop due to a three-node cyclic dependency at injection time. Fixed by using the concrete class instance instead of the IHostedService instance, or make a new keyed service if no concrete class service was requested.

632 of 1673 branches covered (37.78%)

31 of 51 new or added lines in 2 files covered. (60.78%)

8 existing lines in 2 files now uncovered.

1139 of 2408 relevant lines covered (47.3%)

50.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.68
/DI/DependencyInjectionExtensions.cs
1
using Microsoft.Extensions.Configuration;
2
using Microsoft.Extensions.Configuration.Json;
3
using Microsoft.Extensions.DependencyInjection;
4
using Microsoft.Extensions.DependencyInjection.Extensions;
5
using Microsoft.Extensions.FileProviders;
6
using Microsoft.Extensions.Hosting;
7
using Unfucked.DI;
8
#if !NET6_0_OR_GREATER
9
using System.Reflection;
10
using System.Diagnostics;
11
#endif
12

13
namespace Unfucked;
14

15
/// <summary>
16
/// Methods that make it easier to work with the <c>Microsoft.Extensions.Hosting</c> dependency injection/inversion of control library, which is used in the Generic Host and ASP.NET Core.
17
/// </summary>
18
public static partial class DependencyInjectionExtensions {
19

20
    /// <summary>
21
    /// <para>By default, the .NET host only looks for configuration files in the working directory, not the installation directory, which breaks when you run the program from any other directory.</para>
22
    /// <para>Fix this by also looking for JSON configuration files in the same directory as this executable.</para>
23
    /// </summary>
24
    /// <param name="builder">see <c>HostApplicationBuilder.Configuration</c></param>
25
    /// <returns>the same <see cref="IConfigurationBuilder"/> for chaining</returns>
26
    // ExceptionAdjustment: M:System.Collections.Generic.IList`1.Insert(System.Int32,`0) -T:System.NotSupportedException
27
    // ExceptionAdjustment: P:System.Diagnostics.Process.MainModule get -T:System.ComponentModel.Win32Exception
28
    public static IConfigurationBuilder AlsoSearchForJsonFilesInExecutableDirectory(this IConfigurationBuilder builder) {
29
        string? installationDir;
30
        try {
31
            string? processPath;
32
#if NET6_0_OR_GREATER
33
            processPath = Environment.ProcessPath;
1✔
34
#else
35
            processPath = Assembly.GetEntryAssembly()?.Location;
×
36
            if (processPath == null) {
×
37
                using Process currentProcess = Process.GetCurrentProcess();
38
                processPath = currentProcess.MainModule!.FileName;
39
            }
40
#endif
41
            installationDir = Path.GetDirectoryName(processPath);
1✔
42
        } catch (PathTooLongException) {
1✔
43
            return builder;
×
44
        }
45

46
        if (installationDir != null) {
1!
47
            PhysicalFileProvider fileProvider = new(installationDir);
1✔
48

49
            IEnumerable<(int index, IConfigurationSource source)> sourcesToAdd = builder.Sources.SelectMany<IConfigurationSource, (int, IConfigurationSource)>((src, oldIndex) =>
1✔
50
                src is JsonConfigurationSource { Path: {} path } source
2!
51
                    ? [
2✔
52
                        (oldIndex, new JsonConfigurationSource {
2✔
53
                            FileProvider   = fileProvider,
2✔
54
                            Path           = path,
2✔
55
                            Optional       = true,
2✔
56
                            ReloadOnChange = source.ReloadOnChange,
2✔
57
                            ReloadDelay    = source.ReloadDelay
2✔
58
                        })
2✔
59
                    ]
2✔
60
                    : []).ToList();
2✔
61

62
            int sourcesAdded = 0;
1✔
63
            foreach ((int index, IConfigurationSource source) in sourcesToAdd) {
4!
64
                builder.Sources.Insert(index + sourcesAdded++, source);
1✔
65
            }
66
        }
67

68
        return builder;
1✔
69
    }
×
70

71
    /// <summary>
72
    /// <para>Declarative injection of dependencies with shorter lifetimes into dependents with longer lifetimes, like <c>javax.inject.Provider&lt;T&gt;</c>, without the complication of creating scopes, so you don't have a inject an <see cref="IServiceProvider"/> and imperatively request everything, which isn't very DI-like.</para>
73
    /// <para>Configure dependency injection context to allow you to inject <see cref="Provider{T}"/> instances into your dependent services.</para>
74
    /// <para>This allows you to inject not an instance of a dependency service into your consumer, but rather a factory method that lazily provides the dependency service when called in your dependent service.</para>
75
    /// <para>This is useful when the lifetime of the dependent is longer than the lifetime of the dependency, and you want the dependency to get cleaned up. For example, a singleton may depend on a prototype-scoped service that must be eagerly cleaned up after it's used to avoid leaking memory, or because the dependency cannot be reused.</para>
76
    /// <para>Register: <c>
77
    /// appBuilder.Services
78
    ///     .AddInjectableProviders()
79
    ///     .AddTransient&lt;MyDependency&gt;()
80
    ///     .AddSingleton&lt;MyDependent&gt;();
81
    /// </c></para>
82
    /// <para>Inject: <c>
83
    /// public class MyDependent(Provider&lt;MyDependency&gt; dependencyProvider) {
84
    ///     public void Start() {
85
    ///         using MyDependency dependency = dependencyProvider.Get();
86
    ///         dependency.Run();
87
    ///     }
88
    /// }
89
    /// </c></para>
90
    /// </summary>
91
    /// <param name="services">Application builder's <see cref="HostApplicationBuilder.Services"/>.</param>
92
    public static IServiceCollection AddInjectableProviders(this IServiceCollection services) {
93
        services.TryAddSingleton(typeof(Provider<>), typeof(MicrosoftDependencyInjectionServiceProvider<>));
1✔
94
        services.TryAddSingleton(typeof(OptionalProvider<>), typeof(MicrosoftDependencyInjectionServiceProvider<>));
1✔
95
        return services;
1✔
96
    }
97

98
    /// <summary>
99
    /// <para>By default in .NET 6 and later, an uncaught exception in a <see cref="BackgroundService"/> will log a critical error and cause the host application to exit with status code 0. This makes it very difficult to automatically determine if the application crashed, such as when it's run from a script or Task Scheduler.</para>
100
    /// <para>This extension allows you to change the exit code returned by this program when it exits due to a <see cref="BackgroundService"/> throwing an exception. By default, this will return 1 on exceptions, but you can customize the exit code too. The exit code is only changed if a <see cref="BackgroundService"/> threw an exception, so the program will still exit with 0 normally.</para>
101
    /// <para>Usage:</para>
102
    /// <para><code>builder.Services.SetExitCodeOnBackgroundServiceException(1);</code></para>
103
    /// </summary>
104
    /// <param name="services">From <see cref="HostApplicationBuilder.Services"/> or similar.</param>
105
    /// <param name="exitCode">The numeric status code you want the application to exit with when a <see cref="BackgroundService"/> throws an uncaught exception. To customize the exit code for different exceptions, use the overload that takes a function for this parameter.</param>
106
    public static IServiceCollection SetExitCodeOnBackgroundServiceException(this IServiceCollection services, int exitCode = 1) => SetExitCodeOnBackgroundServiceException(services, _ => exitCode);
1✔
107

108
    /// <summary>
109
    /// <para>By default in .NET 6 and later, an uncaught exception in a <see cref="BackgroundService"/> will log a critical error and cause the host application to exit with status code 0. This makes it very difficult to automatically determine if the application crashed, such as when it's run from a script or Task Scheduler.</para>
110
    /// <para>This extension allows you to change the exit code returned by this program when it exits due to a <see cref="BackgroundService"/> throwing an exception. By default, this will return 1 on exceptions, but you can customize the exit code too. The exit code is only changed if a <see cref="BackgroundService"/> threw an exception, so the program will still exit with 0 normally.</para>
111
    /// <para>Usage:</para>
112
    /// <para><code>builder.Services.SetExitCodeOnBackgroundServiceException(exception => exception is ApplicationException ? 1 : 2);</code></para>
113
    /// </summary>
114
    /// <param name="services">From <see cref="HostApplicationBuilder.Services"/> or similar.</param>
115
    /// <param name="exitCodeGenerator">A function that takes the <see cref="Exception"/> thrown by the <see cref="BackgroundService"/> and returns the status code to exit this process with.</param>
116
    public static IServiceCollection SetExitCodeOnBackgroundServiceException(this IServiceCollection services, Func<Exception, int> exitCodeGenerator) {
117
        services.AddHostedService(context => new BackgroundServiceExceptionListener(context, exitCodeGenerator));
2✔
118
        return services;
1✔
119
    }
120

121
    internal class BackgroundServiceExceptionListener(IServiceProvider services, Func<Exception, int> exitCodeGenerator): BackgroundService {
1✔
122

123
        protected override Task ExecuteAsync(CancellationToken stoppingToken) {
UNCOV
124
            IEnumerable<BackgroundService> backgroundServices = services.GetServices<IHostedService>().OfType<BackgroundService>().Where(service => service is not BackgroundServiceExceptionListener);
×
125

UNCOV
126
            services.GetRequiredService<IHostApplicationLifetime>().ApplicationStopped.Register(() => {
×
NEW
127
                if (backgroundServices.Select(service => service.ExecuteTask?.Exception?.InnerException).FirstOrDefault() is {} exception) {
×
UNCOV
128
                    Environment.ExitCode = exitCodeGenerator(exception);
×
UNCOV
129
                }
×
UNCOV
130
            });
×
131

UNCOV
132
            return Task.CompletedTask;
×
133
        }
134

135
    }
136

137
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc