Sending telemetry to ApplicationInsights inside ConfigureServices - c#

I'd like to send some telemetry from within ConfigureServices, for example if I'm missing a config setting I'd like to send an event to AI to track this.
public class Startup
{
public void ConfigureServices( IServiceCollection services)
{
services.AddApplicationInsightsTelemetry();
// Is there a way of getting the telemetry client here to send some telemetry?
// e.g. TelemetryClient.TrackEvent("MyEvent");
}
}
Is there any way of getting hold of the telemetry client created by AddApplicationInsightsTelemetry within ConfigureServices? Is the context even valid at that point?

No, we cannot get hold of the telemetry client created by AddApplicationInsightsTelemetry within ConfigureServices before ConfigureServices method is finished.
You should manually create a telemetry client for your purpose in ConfigureServices method, like below:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var s1 = services.AddApplicationInsightsTelemetry(Configuration["APPINSIGHTS_INSTRUMENTATIONKEY"]);
TelemetryConfiguration configuration = TelemetryConfiguration.CreateDefault();
configuration.InstrumentationKey = Configuration["APPINSIGHTS_INSTRUMENTATIONKEY"];
var telemetryClient = new TelemetryClient(configuration);
telemetryClient.TrackEvent("xxx");
}

Related

Passing IOptions<T> to method in StartUp Configuration

I have set up my project to use the IOptions pattern for reading data from the appSettings file.
I have a class that has the following simple constructor to it:
public PlayClass(IOptions<MySettings> settings)
{
_settings = settings;
}
In my ConfigureServices method I have my config set up here:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MySettings>(options => Configuration.GetSection("MyOptions").Bind(options));
}
When I run or test this, everything works as expected. However, I need to call a method from my play PlayClass inside of ConfigureServices.
What is best way to achieve this?
I had originally thought it would be as simple as the following:
public void ConfigureServices(IServiceCollection services)
{
var x = services.Configure<BitBucketSettings>(options => Configuration.GetSection("BitBucketOptions").Bind(options));
var pc = new PlayClass(x);
pc.MyMethod();
}
But this only results in an error: cannot convert from IServiceCollection to IOptions<MySettings>
It is not clear why you want to create an object of class in Startup class. But you can solve your problem as following.
IServiceCollection is used only for create the dependency graph but to resolve the actual dependencies at runtime, ServiceProvider is needed.
To build ServiceProvider, BuildServiceProvider method needs to be called on ServiceCollection. You can see that in the code below.
public void ConfigureServices(IServiceCollection services)
{
//Register the configuration section in the service collection.
services.Configure<BitBucketSettings>(Configuration.GetSection("BitBucketOptions");
// Register the class in service collection.
services.AddScoped<PlayClass, PlayClass>();
// Build Service Provider
var sp = services.BuildServiceProvider();
// Resolve instance or PlayClass from service builder.
var pc = sp.GetService<PlayClass>();
// Call method on instance of PlayClass
pc.MyMethod();
}
I hope this will help you solve your issue.

Multiple hosts with the same DI container

In C#.NET Core you can create a generic host using the following code:
IHostBuilder builder = new HostBuilder()
.ConfigureServices((context, collection) => {
collection.AddSingleton<IMyClass, MyClass>();
collection.AddHostedService<MyService>();
});
await builder.RunConsoleAsync();
This creates a new instance of MyService with the default DI container.
Now, say that I want to create a new host inside MyService. This is easy enough (a web host in this case):
IWebHost webHost = WebHost.CreateDefaultBuilder()
.UseStartup<MyStartup>()
.Build();
.RunAsync();
This webhost will have its own Dependency Injection container, so it will not have access to all dependencies I've already added to the generic host container: i.e. it will not be able to have IMyClass injected into MyStartup.
I've also tried adding a custom IServiceProviderFactory<> using the following code (based on the .UseDefaultServiceProvider() code where they use IServiceCollection as the builder type):
public class CustomServiceProviderFactory : IServiceProviderFactory<IServiceCollection>
{
private readonly IServiceProvider _provider;
public CustomServiceProviderFactory(IServiceProvider provider)
{
_provider = provider;
}
public IServiceCollection CreateBuilder(IServiceCollection services)
{
return services;
}
public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder)
{
return _provider;
}
}
Then in my HostBuilder I added it through .UseServiceProviderFactory(new CustomServiceProviderFactory(_serviceProvider)), but for some reason the HostedService is instantiated before this is created, causing DI exceptions about not finding the required objects.
However, seeing as WebHost.CreateDefaultBuilder() is now the preferred way to create a webhost (in .NET Core 3.0), and an IWebHostBuilder does not have an option to set a custom IServiceProviderFactory this does seem like a dead end.
How can I have the webhost use the same DI container as the initial generic host?
I've tried to do the same thing and this is what I have landed on. Not fully tested but it does appear to work.
First, in my base/first HostBuilder, add the service collection as a service so an IServiceCollection can be resolved via DI later on.
IHostBuilder builder = new HostBuilder()
.ConfigureServices((ctx, services) =>
{
services.AddSingleton<IMyService, MyService>();
services.AddHostedService<MyApp>();
services.AddSingleton(services);
});
In IHostedService.StartAsync() I create the WebHost. I copied the use of services.Replace from the functionality inside UseDefaultServiceProvider():
IWebHost host = WebHost
.CreateDefaultBuilder()
.ConfigureServices(services =>
{
var options = new ServiceProviderOptions();
services.Replace(ServiceDescriptor.Singleton<IServiceProviderFactory<IServiceCollection>>(new CustomServiceProviderFactory(_services, options)));
})
.UseStartup<MyStartup>()
.Build();
In the constructor of my CustomServicesProvider, I also need to remove any IHostedService services or else it appears you enter an infinite loop of the service starting. When creating the service provider, I add everything from the constructor-passed service collection to the local service collection.
class CustomServiceProviderFactory : IServiceProviderFactory<IServiceCollection>
{
private readonly IServiceCollection _baseServices;
private readonly ServiceProviderOptions _options;
public CustomServiceProviderFactory(IServiceCollection baseServices, ServiceProviderOptions options)
{
_baseServices = baseServices;
_options = options;
_baseServices.RemoveAll<IHostedService>();
}
public IServiceCollection CreateBuilder(IServiceCollection services)
{
return services;
}
public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder)
{
foreach (var service in _baseServices)
{
containerBuilder.Add(service);
}
return containerBuilder.BuildServiceProvider(_options);
}
}
I was then able to create a Controller after adding app.UseRouting() and app.UseEndpoints(...) in my startup class. Injecting IMyService was successfully resolved and I could use it as normal.
You could also test it by just adding app.ApplicationServices.GetRequiredService<IMyService>() in your Startup.Configure() method and see that the correct service is returned.

AddHttpContextAccessor in ConfigureServices vs per HttpClient

Is there any difference between adding the httpContextAccessor one time in ConfigureServices method versus adding the HttpContextAccessor per HttpClient configured.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// FIRST VERSION
services.AddHttpContextAccessor();
// SECOND VERSION
var myService1 = services.AddHttpClient<TestHttpClient1>(c =>
{
c.BaseAddress = new Uri(Configuration["TestHttpClient1"]);
});
myService1.Services.AddHttpContextAccessor();
var myService2 = services.AddHttpClient<TestHttpClient2>(c =>
{
c.BaseAddress = new Uri(Configuration["TestHttpClient2"]);
});
myService2.Services.AddHttpContextAccessor();
}
My guess would be to think that in the second version, we have two singleton, one will be used for class TestHttpClient1 and the other one for TestHttpClient2 but I dont see why we do that because I saw this code in production.
Is there any difference between adding the httpContextAccessor one time in ConfigureServices method versus adding the HttpContextAccessor per HttpClient configured.
No, there's no difference whatsoever. myService1.Services and myService2.Services both reference the same IServiceCollection as the services variable. The first call (services.AddHttpContextAccessor()) will register the service, but the next two calls (myService1.Services.AddHttpContextAccessor() and myService2.Services.AddHttpContextAccessor()) will no-op (do nothing).
To put that all in to context, here's an extract from the source code for AddHttpClient<TClient>(...) (source):
var builder = new DefaultHttpClientBuilder(services, name);
// ...
return builder;
A new instance of DefaultHttpClientBuilder is created, which wraps up the IServiceCollection that's passed in. As this is an extension method, services here refers to the same services as in your ConfigureServices method. This is then exposed through IHttpClientBuilder.Services, which is what you're using when you reference e.g. myService1.Services.
The call to AddHttpContextAccessor uses TryAddSingleton, which will register the service only if it hasn't already been registered (source):
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
In your example, it has already been registered by that first call to services.AddHttpContextAccessor(), which means the next two registration attempts do nothing.

Asp.net Core: middleware to controller conversion issue?

I have a Swagger project where I'm doing OAuth (token provider + verification). Everything is working fine, but the token provider was implemented as middleware based on a sample I found online. I want to convert the token provider middleware to a controller so it shows up in Swagger and users quit bugging me on how to get a token :).
In the startup.cs, I created a TokenProviderOptions object and populated it with values that live in the startup.cs (since they also get passed to the oauth verification part). I was then doing:
app.UseMiddleware<TokenProviderMiddleware>(Options.Create(tokenProviderOptions));
and the middleware was getting the options.
Now that I'm getting rid of the middleware, how can I pass in the tokenProvider options to the controller? Seems kind of weird to put it in DI as a singleton.
You can resolve options from the dependency injection container in controllers and other services using the IOptions<T> interface. For example:
public class TokenProviderController
{
private readonly IOptions<TokenProviderOptions> _options;
public TokenProviderController(IOptions<TokenProviderOptions> options)
{
_options = options;
}
}
You can then access the options values using _options.Value.
The options can be configured in the startup class. Typically you populate them from configuration:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
private IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<TokenProviderOptions>(Configuration);
}
// ...
}
If your options consist of hard-coded values, you can use a delegate to configure the binding:
services.Configure<TokenProviderOptions>(o =>
{
o.Foo = "Bar";
});
For more info check out the documentation on the options pattern.

Configure a dbContext from a webjob

I have a website that takes input from emails that go out to users (an email is sent to everyone, they click a link which calls a controller action). I want to use a webjob to send the emails out, but I need to cycle through all the users in the database to grab the email addresses.
I set up the website and everything is working great. I have the DbContext inheriting from IdentityDbContext here:
public class MooodDbContext : IdentityDbContext<ApplicationUser>
{
public MooodDbContext(DbContextOptions options) : base(options)
{
}
public DbSet<Response> Response { get; set; }
public DbSet<Cow> Cow { get; set; }
public DbSet<Herd> Herd { get; set; }
}
The ConfigureServices is run when I launch the webapp
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<MooodDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MooodConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<MooodDbContext>()
.AddDefaultTokenProviders();
services.AddMvc(options => options.Filters.Add(typeof(UnauthorizedExceptionFilterAttribute)));
services.AddScoped<IDbInitializer, DbInitializer>();
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}
but obviously not when I run the webjob. So, when I try to access anything in the context, I get this exception (after setting context = new context()):
System.InvalidOperationException occurred
HResult=0x80131509
Message=No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions<TContext> object in its constructor and passes it to the base constructor for DbContext.
Source=Microsoft.EntityFrameworkCore
It seems to me, it's because I'm not doing any configuration on the DbContext, but I'm not sure. Is there a better way to accomplish this? Or am I missing something small?
Edit: This is the program.cs of my webjob (minus the emailing functionality, which is cluttery):
private static MooodDbContext _context;
// Please set the following connection strings in app.config for this WebJob to run:
// AzureWebJobsDashboard and AzureWebJobsStorage
private static void Main()
{
_context = new MooodDbContext(new DbContextOptions<MooodDbContext>());
var host = new JobHost();
EmailAllHerds();
host.RunAndBlock();
EmailAllHerds();
}
Couple of assumptions that i made here
web job is a console App built on .net core
So you will have Program.cs file with a main method. So in your
Program.cs
class Program {
// declare a field to store config values
private readonly IConfigurationRoot _configuration;
// Declare a constructor
public Program()
{
var builder = new ConfigurationBuilder()
.SetBasePath(environment.BasePath)
.AddJsonFile($"appsettings.json", true, true)
.AddJsonFile($"appsettings.{environment.EnvironmentName}.json", true, true)
.AddEnvironmentVariables();
_configuration = builder.Build();
}
}
public static void Main(string[] args)
{
var program = new Program();
var serviceCollection = new ServiceCollection();
program.ConfigureServices(serviceCollection);
}
private void ConfigureServices(IServiceCollection services)
{
// now here you can resolve your DbContext similar to web
services.AddDbContext<MooodDbContext>(options =>options.UseSqlServer(_configuration.GetConnectionString("MooodConnection")));
}
}

Categories

Resources