When I create the webhost for an ASP.NET Core application I can specify the Startup class but not an instance.
The constructor of your own Startup class can take parameter which are provided through DI. I know how to register services for DI within ConfigureServices but as that is a member of that class these services are not available for the constructor of my startup class.
How do I register services which will be available as constructor parameter of the Startup class?
The reason is that I have to supply an object instance which must be created outside/before the webhost is created and I do not want to pass it in a global-like style.
Code to create the IWebHost:
this.host = new WebHostBuilder()
.UseConfiguration(config)
.UseKestrel()
.UseIISIntegration()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<WebStartup>()
log.Debug("Run webhost");
this.host.Start();
Constructor of WebStartup:
public WebStartup(IHostingEnvironment env, MyClass myClass)
{
var config = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddEnvironmentVariables()
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
.Build();
...
}
So specifically, how to I register MyClass in this example (which obviously must be done before WebStartup is instanciated by the IWebHost)?
Although Steven's concerns are valid and you should take note of them, it is technically possible to configure the DI container that is used to resolve your Startup class.
ASP.NET hosting uses dependency injection to wire up an instance of your Startup class and also let us add our own services to that container using the ConfigureServices extension method on IWebHostBuilder:
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.ConfigureServices(services => services.AddSingleton<IMyService, MyService>())
.UseStartup<Startup>()
.Build();
host.Run();
and:
public Startup(IMyService myService)
{
myService.Test();
}
In fact, all that UseStartup<WebStartup> does is adding it as a service implementation of IStartup to the hosting DI container (see this).
Please note that instances of your services will be resolved again in the application container as a new instance of the IServiceProvider will be built. The registration of the services will, however, be passed to the application IServiceCollection in your Startup class.
This is a 'chicken or the egg' problem: You can't let the DI container resolve an object graph before it is configured.
Your problem however should not exist, because just as you should strictly separate the registration phase of the container from the resolve-phase (as ASP.NET Core enforces upon you), the same way should you separate the registration phase from the phase before that where you load configuration values.
What this means is that classes that you require during the registration phase of the container should not be resolved by the container. because that could lead to common problems that are hard to track down.
Instead you should create the class by hand. For instance:
public class Startup
{
private static MyDependency dependency;
public Startup(IHostingEnvironment env)
{
dependency = new MyDependency();
var instance = new MyClass(dependency);
var builder = new ConfigurationBuilder()
.SetBasePath(instance.ContentRootPath)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Register the dependency if it is required by other components.
services.AddSingleton<MyDependency>(dependency);
// Add framework services.
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
// etc.
}
You have an instance of MyClass called myClass and you need to inject that instance into your Startup class. I had a similar requirement and after some experimentation I came up with the following.
First, we inject the myClass instance before we call UseStartup:
var host = new WebHostBuilder()
// .... other code
.ConfigureServices(services => services.AddSingleton(myClass)) // Inject myClass instance
.UseStartup<WebStartup>()
Now we need to get hold of the object in Startup (WebStartup in your case). I wasn't able to find a way to access it from the constructor, but that shouldn't matter as it can be accessed from within the startup class' ConfigureServices() and, if necessary, saved to a field or property from there if it need to be made available to Startup.Configure() which is called later. Here is what I came up with:
// This goes into Startup.cs/WebStartup.cs
public virtual void ConfigureServices(IServiceCollection services)
{
var myClass = services.Single(s => s.ServiceType == typeof(MyClass)).ImplementationInstance as MyClass;
if (myClass == null)
throw new InvalidOperationException(nameof(MyClass) + " instance is null");
// Now do all the things
}
I suspect that there may be something in the Framework to retrieve the injected instance more easily, but if not - or until I find it - I can confirm that the above works perfectly!
Related
All of our business services were previously set up to use Dependency Injection with IOptions because they were being consumed by ASP.NET Core apps, like so:
NotificationDataAccess.cs:
public class NotificationDataAccess : BaseDataAccess, INotificationDac<Notification>
{
public NotificationDataAccess(IOptions<DataAccessConfiguration> options, IClaimsAccessor claimsAccessor) :
base(options, claimsAccessor)
{
}
}
NotificationBusinessService.cs:
public class NotificationBusinessServices : INotificationServices<Notification>
{
private readonly INotificationDac<Notification> _notificationDataAccess;
public NotificationBusinessServices(
INotificationDac<Notification> notifcationDataAccess)
{
_notificationDataAccess = notifcationDataAccess;
}
}
Now I'm left with the unenviable task of trying to figure out how to leverage the same pattern from a windows service, which doesn't benefit from the built-in ASP.NET Core features for handling DI. When the service starts up, I execute the following code:
// Set up configuration, services, and logging.
IServiceCollection services = new ServiceCollection();
var startup = new Startup();
startup.ConfigureServices(services);
IServiceProvider serviceProvider = services.BuildServiceProvider();
var configuration = serviceProvider.GetService<IConfigurationRoot>();
var notificationService = serviceProvider.GetService<INotificationServices<Notification>>();// TODO: This errors!
processor = new Processor(configuration, notificationService);
And here is the Startup.cs code, which is supposed to configure the services:
public class Startup
{
IConfigurationRoot Configuration { get; }
public Startup()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Path.Combine(AppContext.BaseDirectory))
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfigurationRoot>(Configuration);
//services.AddMvc();
// Add application services.
Listings.Business.Configuration.Instance = new BusinessLayerConfiguration();
services.Configure<DataAccessConfiguration>(options => Configuration.GetSection("Data").Bind(options));
services.AddScoped(typeof(INotificationDac<Notification>), typeof(NotificationDataAccess));
services.AddScoped(typeof(INotificationServices<Notification>), typeof(NotificationBusinessServices));
}
}
Unfortunately, when I run the windows service it throws an exception when trying to get the notificationService:
var notificationService = serviceProvider.GetService<INotificationServices<Notification>>();
The exception is:
System.InvalidOperationException: 'Unable to resolve service for type
'Microsoft.Extensions.Options.IOptions`1[Rpr.Listings.DataAccess.DataAccessConfiguration]'
while attempting to activate
'Rpr.Listings.DataAccess.NotificationDataAccess'.'
I was hoping my "services.Configure" code would resolve this, but alas no. Clearly I need to register IOptions in my Startup.cs, however I have no idea how to do so. Is this something that usually happens out of the box with ASP.NET MVC? Does "services.AddMvc();" normally register this binding correctly? I can call that, but would need to import a ton of ASP.NET MVC packages into my windows service, which I'm reluctant to do.
Please let me know how to register the IOptions binding correctly, thanks!
It turns out that all I was missing was:
services.AddOptions();
Once I added that, the IOptions binding was registered correctly!
In case it helps anyone, I had this issue in a console app and it was caused by creating the service provider
IServiceProvider serviceProvider = services.BuildServiceProvider();
before I'd registered my config
services.Configure<DataAccessConfiguration>(Configuration.GetSection("Data"));
I've installed and configured Hangfire in my .NET Core web application's Startup class as follows (with a lot of the non-Hangfire code removed):
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseHangfireServer();
//app.UseHangfireDashboard();
//RecurringJob.AddOrUpdate(() => DailyJob(), Cron.Daily);
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.Configure<AppSettings>(Configuration);
services.AddSingleton<IConfiguration>(Configuration);
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IPrincipal>((sp) => sp.GetService<IHttpContextAccessor>().HttpContext.User);
services.AddScoped<IScheduledTaskService, ScheduledTaskService>();
services.AddHangfire(x => x.UseSqlServerStorage(connectionString));
this.ApplicationContainer = getWebAppContainer(services);
return new AutofacServiceProvider(this.ApplicationContainer);
}
}
public interface IScheduledTaskService
{
void OverduePlasmidOrdersTask();
}
public class ScheduledTaskService : IScheduledTaskService
{
public void DailyJob()
{
var container = getJobContainer();
using (var scope = container.BeginLifetimeScope())
{
IScheduledTaskManager scheduledTaskManager = scope.Resolve<IScheduledTaskManager>();
scheduledTaskManager.ProcessDailyJob();
}
}
private IContainer getJobContainer()
{
var builder = new ContainerBuilder();
builder.RegisterModule(new BusinessBindingsModule());
builder.RegisterModule(new DataAccessBindingsModule());
return builder.Build();
}
}
As you can see, I'm using Autofac for DI. I've set things up to inject a new container each time the Hangfire job executes.
Currently, I have UseHangfireDashboard() as well as the call to add my recurring job commented out and I'm receiving the following error on the line referencing IPrincipal:
System.NullReferenceException: 'Object reference not set to an instance of an object.'
I understand that Hangfire does not have an HttpContext. I'm not really sure why it's even firing that line of code for the Hangfire thread. I'm ultimately going to need to resolve a service account for my IPrincipal dependency.
How can I address my issue with Hangfire and HttpContext?
The main problem I'm having now is when I add UseHangfireServer, I
then need to resolve HttpContext too
Found here Using IoC containers
HttpContext is not available
Request information is not available during the instantiation of a
target type. If you register your dependencies in a request scope
(InstancePerHttpRequest in Autofac, InRequestScope in Ninject and so
on), an exception will be thrown during the job activation process.
So, the entire dependency graph should be available. Either register
additional services without using the request scope, or use separate
instance of container if your IoC container does not support
dependency registrations for multiple scopes.
resolving scoped dependencies in .net core would require a request which is not available during startup when registering and activating jobs. Therefore make sure that your service required for activation during startup are not registered using scoped lifetimes.
services.AddTransient<IScheduledTaskManager, ScheduledTaskManageImplementation>();
All that is left now is to configure the application to use that service with the recurring job,
public class Startup {
public IContainer ApplicationContainer { get; private set; }
public Startup(IHostingEnvironment env) {
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public void Configuration(IApplicationBuilder app) {
// app.AddLogger...
//add hangfire features
app.UseHangfireServer();
app.UseHangfireDashboard();
//Add the recurring job
RecurringJob.AddOrUpdate<IScheduledTaskManager>(task => task.ProcessDailyJob(), Cron.Daily);
//app.UseMvc...
//...other code
}
public IServiceProvider ConfigureServices(IServiceCollection services) {
// Adding custom services
services.AddTransient<IScheduledTaskManager, ScheduledTaskManageImplementation>();
//add other dependencies...
// add hangfire services
services.AddHangfire(x => x.UseSqlServerStorage("<connection string>"));
//configure Autofac
this.ApplicationContainer = getWebAppContainer(services);
//get service provider
return new AutofacServiceProvider(this.ApplicationContainer);
}
IContainer getWebAppContainer(IServiceCollection service) {
var builder = new ContainerBuilder();
builder.RegisterModule(new BusinessBindingsModule());
builder.RegisterModule(new DataAccessBindingsModule());
builder.Populate(services);
return builder.Build();
}
//...other code
}
References
Hangfire 1.6.0
Integrate HangFire With ASP.NET Core
Using IoC containers
Why is Hangfire trying to resolve the .NET Core Startup class?
Hangfire doesn't store lambda expressions in the database, it stores the type and method being called. Then when the scheduled task is due to run, it resolves the type from the container and calls the method.
In your case, the method is on Startup.
You can register Startup with Autofac if you want, but it's probably easiest to have a scheduled task service:
AddOrUpdate<IScheduledTaskService>(x => x.DailyTask(), Cron.Daily);
I'm not sure of the type for jobmanager off the top of my head, but you can resolve the dependency from the container using a scope. You'll want to resolve from the scope in a using statement to prevent memory leaks. See the Autofac Docs
// not sure what type "jobManager" is
TYPE jobManager;
using(var scope = ApplicationContainer.BeginLifetimeScope())
{
jobManager = scope.Resolve<TYPE>();
}
RecurringJob.AddOrUpdate( ... );
I'm trying to get some of my services in the configure method so I can seed my database easier with them.
So I've injected IServiceProvider in my Configure method but every time I do : var service = serviceProvider.GetService<UserService>(); it returns null...
Here's my code :
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(this.Configuration.GetConnectionString("DbConnectionString")));
// Add framework services.
services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<IdentityDbContext>().AddDefaultTokenProviders();
string connection = this.Configuration.GetConnectionString("DbConnectionString");
services.AddEntityFramework();
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connection));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.Configure<AppSettings>(this.Configuration.GetSection("AppSettings"));
services.AddScoped<IsAuthorized>();
services.AddSingleton<UserManager<ApplicationUser>>();
services.AddTransient<IUsersService, UserService>();
services.AddMvc(config => { config.Filters.Add(typeof(SingletonUsers)); });
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IServiceProvider services)
{
loggerFactory.AddConsole(this.Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseMvc();
// This returns the context.
var context = services.GetService<ApplicationDbContext>();
// This returns null.
var userService = services.GetService<UserService>();
// Can't work without the UserService
MyDbInitializer.Initialize(context, userService);
}
}
as you registered UserService using
services.AddTransient<IUsersService, UserService>();
instead of
var userService = services.GetService<UserService>();
you need to ask for interface type:
var userService = services.GetService<IUserService>();
As an alternative to injecting IServiceProvider, you can just request the services as parameters to Configure:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
ApplicationDbContext context,
IUserService userService)
{
}
IServiceProvider is not registered with the Di/IoC, because the IServiceProvider is the IoC/DI (or a wrapper around it, when using 3rd party DI).
Once a IServiceProvider is created, it's dependency configuration can't be changed anymore (this applies to the out of the box IoC).
When you need a dependency in Configure you should pass this dependency as the method parameter (method injection) and get the instance. If you still need to access the IServiceProvider, you can do so by calling app.ApplicationServices.
But be aware, when you use app.ApplicationServices, you are resolving from the application-wide container. Every scoped or transient service resolved this way, will stay active until the application ends. This is because during application startup there is no scoped container, it is created during a request.
This means, you have to create a scope within Configure method, instantiate the services you need, call them and then dispose the scoped context before you leave it.
// Example of EF DbContext seeding I use in my application
using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
using (var context = scope.ServiceProvider.GetRequiredService<MyDbContext>())
{
if(context.Database.EnsureCreated())
{
context.SeedAsync().Wait();
}
}
}
This makes sure that all services are clearly disposed and you have no memory leaks. This can have severe impact if/when you initialize DbContext without creating a scoped container, like turning DbContext into a singleton (because it will be resolved from parent container) or causing memory leaks because services resolved in application scope remain active for the lifetime of the application.
Here's the skeleton of a standard ASP.NET Core application:
var config = new ConfigurationBuilder()
.AddCommandLine(args)
.AddEnvironmentVariables(prefix: "ASPNETCORE_")
.Build();
var host = new WebHostBuilder()
.UseConfiguration(config)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
In this piece the ASP.NET Core apparatus instantiates an instance of Startup.cs class
.UseStartup<Startup>()
My query is how can I get hold (reference) of this already instantiated instance of Startup object that I can plug into my Library/Framework.
Context is to setup some Uber level framework and get a reference of this junction (Startup.cs) where all the requests are getting initiated.
If your Startup implements IStartup interface, getting reference to it is easy:
var host = new WebHostBuilder()
.UseConfiguration(config)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
var startup = host.Services.GetService(typeof(IStartup)); // or from any other part of code using IServiceProvider.
However, asp.net core does not require your startup class to implement this interface. If it does not - it will use adapter pattern and adapt your Startup class to IStartup interface. You will still have an instance of IStartup, but it will not be your Startup class. Instead it will be an instance of ConventionBasedStartup. Asp.net core will explore methods of your startup class, find Configure and ConfigureServices methods and will pass them to ConventionBasedStartup which will adapt them to IStartup interface. In this case, it's not possible to retrieve instance of your startup class without heavy reflection, because it's not actually stored in any field (even in private) of ConventionBasedStartup and is only reachable through delegate references.
Long story short - if you want to get instance of your Startup class - make it implement IStartup interface.
Update about how to implement IStartup interface:
public class Startup : IStartup
{
public Startup(IHostingEnvironment env)
{
// constructor as usual
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
public void Configure(IApplicationBuilder app) {
app.UseMvc();
// resolve services from container
var env = (IHostingEnvironment) app.ApplicationServices.GetService(typeof(IHostingEnvironment));
var logger = (ILoggerFactory)app.ApplicationServices.GetService(typeof(ILoggerFactory));
logger.AddConsole(Configuration.GetSection("Logging"));
logger.AddDebug();
// etc
}
public IServiceProvider ConfigureServices(IServiceCollection services) {
services.AddMvc();
// etc
// return provider
return services.BuildServiceProvider();
}
}
I'm trying to configure kestrel so that when it's in it's raw mode it runs on a specific port. However to do so it appears that the launchsettings.json needs to pass command line args to do so since there is no straight up option and it always runs on port 5000 which will obviously conflict if you have an api you need to run and a website.
So I added the CommandLine package to my site and you can indeed use builder.AddCommandLine() in the startup.cs file.
The problem is how to get the args from the program.cs to the Startup.cs or look them up other than a static variable.
Kind of makes the extension method pointless if you can't get at the args.
Any better ways of doing this?
A simple solution is to access the command line arguments through the Environment.GetCommandLineArgs method.
You only need to make sure that you remove the first argument, which is the executable name:
public class Startup
{
public Startup(IHostingEnvironment env)
{
var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
var builder = new ConfigurationBuilder();
builder.AddCommandLine(args);
Configuration = builder.Build();
}
}
UPDATE
I actually found what seems more elegant solution:
Parse command line arguments into IConfigurationRoot in Program (using CommandLineApplication, good article & examples here)
Just pass this IConfigurationRoot to Startup via DI container.
Like so:
public static IWebHost BuildWebHost(string[] args)
{
var configuration = LoadConfiguration(args);
// Use Startup as always, register IConfigurationRoot to services
return new WebHostBuilder()
.UseKestrel()
.UseConfiguration(configuration)
.ConfigureServices(s => s.AddSingleton<IConfigurationRoot>(configuration))
.UseStartup<Startup>()
.Build();
}
public class Startup
{
public Startup(IConfigurationRoot configuration)
{
// You get configuration in Startup constructor or wherever you need
}
}
Example implementation of LoadConfiguration, which parses args and builds IConfigurationRoot (in this example configuration file name can be overridden in command line arguments):
private static IConfigurationRoot LoadConfiguration(string[] args)
{
var configurationFileName = "configuration.json";
var cla = new CommandLineApplication(throwOnUnexpectedArg: true);
var configFileOption = cla.Option("--config <configuration_filename>", "File name of configuration", CommandOptionType.SingleValue);
cla.OnExecute(() =>
{
if (configFileOption.HasValue())
configurationFileName = configFileOption.Value();
return 0;
});
cla.Execute(args);
return new ConfigurationBuilder()
.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
.AddJsonFile(configurationFileName, optional: false, reloadOnChange: true)
.AddCommandLine(args)
.Build();
}
OLD ANSWER
You can instantiate Startup class by yourself and pass it as instances to WebHostBuilder. It is somewhat not so elegant, but doable. From here.
public static IWebHost BuildWebHost(string[] args)
{
// Load configuration and append command line args
var config = new ConfigurationBuilder()
.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
.AddJsonFile("configuration.json")
.AddCommandLine(args)
.Build();
// pass config to Startup instance
var startup = new Startup(config);
// Instead of using UseStartup<Startup>()
// Register startup to services
return new WebHostBuilder()
.UseKestrel()
.UseSetting("applicationName", "Your.Assembly.Name")
.UseConfiguration(config)
.ConfigureServices(services => services.AddSingleton<IStartup>(startup))
.Build();
}
Couple of caveats are:
by doing so, Startup should implement IStartup which is limited for Configure method in parameters to only Configure(IApplicationBuilder app) instead of full Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime lifetime)
For some reason, you need to specify applicationName parameter manually as in my example. I'm testing this on 2.0.0-preview1-final
Kestrel can be configured to listen on a different port in several ways. None of these methods need to happen in the Startup class, but rather in the Main method of the Program class. Using the AddCommandLine extension method is one of them. To use that, modify your Program.cs file's Main method to look something like this:
public static void Main(string[] args)
{
var config = new ConfigurationBuilder()
.AddCommandLine(args)
.Build();
var host = new WebHostBuilder()
.UseKestrel()
.UseConfiguration(config)
.UseStartup<Startup>()
.Build();
host.Run();
}
Then, run the application with dotnet run --server.urls http://*:<yourport>, replacing <yourport> with the actual port number that you want it to run on. The * makes it listen on all available IP addresses, if you want to listen on a specific address then you need to specify it there instead of the *.
Another option for changing the port is to use the .UseUrls method to hard-code the port and address. For example:
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseUrls("http://*:8080")
.UseStartup<Startup>()
.Build();
host.Run();
}
This example will make your application listen on port 8080 on all available IP addresses.