Activity is null when using Microsoft Hosting Extensions and net472 - c#

I am trying to use OpenTelemetry with my net472 app that uses Microsoft.Extensions.Hosting.
I create my host like this:
Host.CreateDefaultBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddOpenTelemetry().WithTracing(tracerProviderBuilder =>
{
tracerProviderBuilder
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("MySample"))
.AddConsoleExporter()
.AddSource(serviceName);
}).StartWithHost();
})
.Build();
If I then try to create a new activity like this, it is null:
var activitySource = new ActivitySource(serviceName);
using var activity = activitySource.StartActivity("Hello");
If instead I register OpenTelemetry like this, it works just fine:
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("MySample"))
.AddSource(serviceName)
.AddConsoleExporter()
.Build();
How can I get an ActivitySource that has the configured listener using the first approach of creating a Host?

The solution that worked for me is to resolve the TracerProvider using the ServiceCollection. That way the listener gets subscribed and ActivitySource is able to start activities.
This is how I register the services.
Host.CreateDefaultBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddSingleton(new ActivitySource(serviceName));
services.AddOpenTelemetry().WithTracing(tracerProviderBuilder =>
{
tracerProviderBuilder
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("MySample"))
.AddConsoleExporter()
.AddSource(serviceName);
});
})
.Build();
And then when TracerProvider is resolved, it's build using the configured TracerProvider:
using var tracerProvider = ServiceLocator.GetService<TracerProvider>();
var activitySource = ServiceLocator.GetService<ActivitySource>();
// Now this doesn't provide a null object
using var activity = activitySource.StartActivity("Hello");
Just for reference, this is ServiceLocator:
public static class ServiceLocator
{
internal static IHost Host { get; set; }
public static T GetService<T>() where T : class
{
return (T)Host.Services.GetService(typeof(T));
}
}

Thanks for the hint, i tried to adopt this on my .net core 7 application.
The following code seems to fix the bug for me too.
var app = builder.Build();
...
app.UseTelemetry();
...
app.Run();
public static void UseTelemetry(this IApplicationBuilder app)
{
_ = app.ApplicationServices.GetRequiredService<TracerProvider>();
}

Related

How to configure HealthCheck with IHost?

In our current implementation of healthcheck's in worker service we do like this (simplified)
var options = new WebApplicationOptions {
Args = args,
ContentRootPath = WindowsServiceHelpers.IsWindowsService()
? AppContext.BaseDirectory
: default
};
var builder = WebApplication.CreateBuilder(options);
builder.Host.UseWindowsService();
builder.Services.AddHealthChecks().AddCheck<ServiceIsOnlineCheck>(nameof(ServiceIsOnlineCheck));
builder.Services.AddHostedService<Worker>();
var healthcheckoptions = new HealthCheckOptions
{
ResponseWriter = ResponseWriters.WriteDetailedStatus,
ResultStatusCodes =
{
[HealthStatus.Healthy] = StatusCodes.Status200OK,
[HealthStatus.Degraded] = StatusCodes.Status200OK,
[HealthStatus.Unhealthy] = StatusCodes.Status200OK
}
};
var app = builder.Build();
app.UseHealthChecks("/health", healthcheckoptions);
app.Run();
When I create a new worker service in .NET 7, the setup in program.cs is completely different and I can not understand how we can set up health checks in them.
How do you implement it when program.cs looks like this? (we need to set our own response writer and other custom options)
IHost host = Host.CreateDefaultBuilder(args)
.UseWindowsService(options =>
{
options.ServiceName = "Service Name";
})
.ConfigureWebHost(host =>
{
// ???
})
.ConfigureServices(services =>
{
services.AddHostedService<RunOnScheduleWorker>();
})
.Build();
host.Run();
This template uses the generic hosting (which was used in pre .NET 6 templates), so you can setup it with Startup. Here is a small working snippet which you can draw inspiration from:
IHost host = Host.CreateDefaultBuilder(args)
.UseConsoleLifetime()
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
})
.ConfigureServices(services => { services.AddHostedService<Worker>(); })
.Build();
host.Run();
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHealthChecks();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHealthChecks("/health");
}
}
But you are not limited to using it, you can:
Switch to the one previously used, just copy-paste everything from the one you used.
Since you want to expose ASP.NET Core endpoint(s) - use corresponding project type and add just hosted service to it.
Read more:
.NET Generic Host
.NET Generic Host in ASP.NET Core
The Startup class

.NET 6 IWebHost vs IHost vs WebApplication: Startup class with ILogger Dependency Injection

I migrated a .NET WebApp from originally 2.1 to 3.1 and 6.0.
I would like to switch to the new 'minimal hosting model' from .NET 6 but still want to keep my long and complex Startup class separated.
I am using Serilog with custom configuration as ILogger implementation: I create a LoggerBuilder in a separate class.
My main looks like this:
public static async Task Main(string[] args)
{
// global shared logger, created BEFORE the host build to be able to log starting and ending the service.
Log.Logger = LoggerBuilder.BuildLogger();
try
{
LoggerAudit.LogAuditAsWarning("Starting " + Constants.ServiceName);
using var source = new CancellationTokenSource();
await CreateWebHost(args).RunAsync(source.Token).ConfigureAwait(false);
//await CreateHost(args).RunAsync(source.Token).ConfigureAwait(false);
//await CreateWebApp(args).RunAsync(source.Token).ConfigureAwait(false);
}
catch (Exception ex)
{
LoggerAudit.GlobalLogAudit.Fatal(ex, Constants.ServiceName + " terminated unexpectedly");
}
finally
{
LoggerAudit.LogAuditAsWarning("Closing " + Constants.ServiceName);
Log.CloseAndFlush();
}
}
with 3 implementations of the Host or WebApp, which should be equivalent
private static IWebHost CreateWebHost(string[] args) // .NET Core 2.1 way
{
IWebHostBuilder builder = WebHost.CreateDefaultBuilder(args)
.UseSerilog()
.SuppressStatusMessages(true)
.ConfigureAppConfiguration((hostContext, config) =>
{
config.BuildConfiguration();
})
.UseStartup<Startup>()
.UseUrls("http://*:5000");
return builder.Build();
}
private static IHost CreateHost(string[] args) // .NET Core 3.1 way
{
IHostBuilder builder = Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.SuppressStatusMessages(true)
.ConfigureAppConfiguration((hostContext, config) =>
{
config.BuildConfiguration();
})
.UseStartup<Startup>()
.UseUrls("http://*:5000");
});
return builder.Build();
}
private static WebApplication CreateWebApp(string[] args) // .NET 6 way
{
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
var startup = new Startup(builder.Configuration, null);
startup.ConfigureServices(builder.Services);
WebApplication app = builder.Build();
IApiVersionDescriptionProvider provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
startup.Configure(app, app.Environment, provider, app.Lifetime);
return app;
}
All 3 ways are still compatible with .NET 6 and suppose to work fine.
Now, UseSerilog() method tells me the WebHost way is obsolete, so I should migrate. So it makes sense to migrate to the newest model with WebApplication.
The catch is the constructor signature of my Startup class
public Startup(IConfiguration config, ILogger<Startup> logger)
I used to still have the CreateWebHost version active.
Switching to CreateHost throws an exception in the Startup constructor:
"Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger`1[Siemens.BT.Edge.CloudConnector.Startup]' while attempting to activate 'Siemens.BT.Edge.CloudConnector.Startup'."
WHY ?
To avoid the exception, I can use the Startup factory:
.UseStartup(builderContext => new Startup(builderContext.Configuration, null))
Then the question is: how can I use my ILogger there with Dependency Injection?
And the same apply when switching to CreateWebApp.
In any other situation, I should use the
var logger = app.Services.GetService<ILogger<Startup>>();
but obviously, I can't call that before calling the Build method and Startup ctor.
So the better question would be: why did my Startup constructor work with WebHost?
so the answer for the WebApplication is to use the BuildServiceProvider:
private static WebApplication CreateWebApp(string[] args) // .NET 6
{
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
ILogger<Startup> logger = builder.Services.BuildServiceProvider().GetService<ILogger<Startup>>();
var startup = new Startup(builder.Configuration, logger);
startup.ConfigureServices(builder.Services);
WebApplication app = builder.Build();
IApiVersionDescriptionProvider provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
startup.Configure(app, app.Environment, provider, app.Lifetime);
return app;
}
It is working but looks a bit ugly...

How to inject a service into Blazor?

I'm having trouble trying to inject some services into the client project. In the Program.cs file I have the following:
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
ConfigureServices(builder.Services);
}
public static void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IMetamaskInterop, MetamaskBlazorInterop>();
services.AddSingleton<MetamaskService>();
services.AddSingleton<MetamaskInterceptor>();
}
}
In a new component I try to inject these services as follows:
#inject IJSRuntime JSRuntime;
#inject MetamaskService metamaskService;
#inject MetamaskInterceptor metamaskInterceptor;
But doing this is giving me an error and it is the following:
I appreciate any help or guidance.
You are configuring the services after you start you app.
Add ConfigureServices(builder.Services); before await builder.Build().RunAsync();.
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// configure services before running the app
ConfigureServices(builder.Services);
await builder.Build().RunAsync();
}
public static void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IMetamaskInterop, MetamaskBlazorInterop>();
services.AddSingleton<MetamaskService>();
services.AddSingleton<MetamaskInterceptor>();
}
}
You shouldn't introduce a new static method named ConfigureServices into the Program class. This is not the reason for the exception thrown, but it is greatly misleading as ConfigureServices is the name of a method defined in the Startup class (used in the past in WebAssembly Blazor, and then removed...).
Sometimes, you may need a second method for conditional configuration, but don't name it ConfigureServices. In the current instance, you don't need a second method. Just do it as it is done below.
When you use a second method, you mustn't run
await builder.Build().RunAsync(); as this code build and execute your app, rendering it too late to add more services to the service collection. Just pass the IServiceCollection object to the second method, and when it returns build and run your app.
Here's the updated version of your code:
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddSingleton<IMetamaskInterop, MetamaskBlazorInterop>();
builder.Services.AddSingleton<MetamaskService>();
builder.Services.AddSingleton<MetamaskInterceptor>();
await builder.Build().RunAsync();
}
}

AddOpenIdConnect with an External Configuration Service

I am adding OpenIdConnect to my app like so:
.AddOpenIdConnect("oidc", options =>
{
var clientSecret = Configuration.GetValue<string>("clientSecret");
options.ClientSecret = clientSecret;
});
I'd like to be able to use another service to get the secret like this:
.AddOpenIdConnect("oidc", (services, options) =>
{
var secretService = services.GetService<ISecretService>();
var clientSecret = secretService.Get("clientSecret");
options.ClientSecret = clientSecret;
});
I saw there is use of app.UseOpenIdConnectAuthentication but I don't see it in the nuget package.
I have the following installed:
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.1.0" />
How can I do this?
It is possible to execute a post configuration class that can inject services.
Like so:
public class OpenIdConnectPostConfigureOptions : IPostConfigureOptions<OpenIdConnectOptions>
{
private readonly ISecretsService _secretsService;
public OpenIdConnectPostConfigureOptions(ISecretsService secretsService)
{
_secretsService = secretsService;
}
public async void PostConfigure(string name, OpenIdConnectOptions options)
{
options.ClientSecret = await _secretsService.Get("clientSecret");
}
}
In the described case I'd recommend extending Configuration rather than using DI in the Action.
To access secrets you can add Configuration providers and continue to use Configuration.GetValue in ConfigureServices method.
For Azure Key-Vault it is under Microsoft.Extensions.Configuration.AzureKeyVault nuget package.
public static void Main(string[] args)
{
CreateWebHostBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
if (env.IsLocal())
{
...
}
else
{
config.AddAzureKeyVault(keyVaultUri);
}
})
.Build()
.Run();
}
For AWS - Amazon.Extensions.Configuration.SystemsManager

How do you unit test RazorViewEngineOptions in ASP.NET Core MVC?

In an extension method called from ConfigureServices I'm adding an instance of EmbeddedFileProvider to RazorViewEngineOptions. I'd like to test that it gets added but I can't find how to get the RazorViewEngineOptions instance.
This works when the application is run:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddMyServices(Configuration);
}
public static IServiceCollection AddMyServices(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProviders.Add(new EmbeddedFileProvider(typeof(MyClass).Assembly, "My.Namespace"));
});
return services;
}
But how do I test it? A NullReferenceException is thrown here:
[Fact]
public void MyTest()
{
var services = new ServiceCollection();
var serviceProvider = services.BuildServiceProvider();
MyServicesBuilder.AddMyServices(services, new Mock<IConfiguration>().Object);
var razorOptions = serviceProvider.GetService<IOptions<RazorViewEngineOptions>>();
Assert.Equal(1, razorOptions.Value.FileProviders.Where(x => x.GetType() == typeof(EmbeddedFileProvider)).Count());
}
I have tried adding services.AddMvc() or services.AddSingleton<RazorViewEngineOptions>().
I've also tried calling services.GetRequiredService<RazorViewEngineOptions>() but that throws System.InvalidOperationException : No service for type 'Microsoft.Extensions.Options.IOptions'1[Microsoft.AspNetCore.Mvc.Razor.RazorViewEngineOptions
I've also tried asking for RazorViewEngineOptions rather than IOptions<RazorViewEngineOptions>.
Anything added to the service collection after the provider has already been built, wont be known to the provider.
Added everything needed to the service collection and only then build the provider to perform your assertions
For example
[Fact]
public void MyTest() {
//Arrange
var services = new ServiceCollection();
services.AddOptions();
IConfiguration config = new ConfigurationBuilder()
// Call additional providers here as needed.
//...
.Build();
//Act
MyServicesBuilder.AddMyServices(services, config);
//OR
//services.AddMyServices(config);
//Assert
var serviceProvider = services.BuildServiceProvider();
var razorOptions = serviceProvider.GetService<IOptions<RazorViewEngineOptions>>();
Assert.NotNull(razorOptions);
Assert.Equal(1, razorOptions.Value.FileProviders.Where(x => x.GetType() == typeof(EmbeddedFileProvider)).Count());
}

Categories

Resources