DNN 10 supports real constructor injection in WebForms module controls. Dependencies should be declared explicitly as constructor parameters instead of being resolved through Globals.DependencyProvider, PortalModuleBase.DependencyProvider, or another service-locator pattern.
Requirements
This approach requires:
- DNN Platform 10.0 or later.
- A compiled WebForms module.
- Services registered through
IDnnStartup.
- A public module control with a public constructor.
DNN 10 registers its service provider with HttpRuntime.WebObjectActivator. When ASP.NET creates a WebForms control, DNN uses the current request scope and ActivatorUtilities to resolve its constructor dependencies.
The module does not need to configure HttpRuntime.WebObjectActivator or register the module control itself.
1. Define the service
Keep the interface independent of WebForms and DNN UI classes.
public interface IArticleService
{
Article GetArticle(int articleId);
}
public sealed class ArticleService : IArticleService
{
private readonly IArticleRepository articleRepository;
public ArticleService(IArticleRepository articleRepository)
{
this.articleRepository = articleRepository;
}
public Article GetArticle(int articleId)
{
return this.articleRepository.GetArticle(articleId);
}
}
Constructor injection should be used throughout the dependency graph. ArticleService therefore receives IArticleRepository instead of resolving it from an IServiceProvider.
2. Register the services
Create an IDnnStartup implementation in the module assembly.
using DotNetNuke.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
public sealed class Startup : IDnnStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IArticleRepository, ArticleRepository>();
services.AddScoped<IArticleService, ArticleService>();
}
}
DNN discovers IDnnStartup implementations during application startup and calls ConfigureServices.
The Startup class itself should:
- Be concrete and discoverable in the deployed module assembly.
- Have a public parameterless constructor.
- Only register services.
- Not resolve services or perform application work during startup.
3. Inject the service into the WebForms module
Declare every required dependency in the module control constructor.
using System;
using DotNetNuke.Entities.Modules;
public partial class View : PortalModuleBase
{
private readonly IArticleService articleService;
public View(IArticleService articleService)
{
this.articleService = articleService
?? throw new ArgumentNullException(nameof(articleService));
}
protected void Page_Load(object sender, EventArgs e)
{
if (this.IsPostBack)
{
return;
}
var articleId = this.ModuleId;
var article = this.articleService.GetArticle(articleId);
this.TitleLabel.Text = article.Title;
this.ContentLiteral.Text = article.Content;
}
}
The module control does not need to be registered explicitly:
// Not required:
services.AddTransient<View>();
If the control type is not registered, DNN uses ActivatorUtilities.CreateInstance to construct it and resolve its constructor parameters from the current request scope.
Do not use a service locator
The following is not constructor injection:
public partial class View : PortalModuleBase
{
private readonly IArticleService articleService;
public View()
{
this.articleService =
this.DependencyProvider.GetRequiredService<IArticleService>();
}
}
The class still has a hidden dependency on the global container. This pattern:
- Hides the actual dependencies of the class.
- Allows missing dependencies to fail later at runtime.
- Couples the class to DNN’s service-provider infrastructure.
- Makes isolated unit testing more difficult.
- Encourages dependencies to be resolved from arbitrary locations.
- Makes service lifetimes and scope ownership less clear.
Injecting IServiceProvider and resolving services from it has the same disadvantages:
public View(IServiceProvider serviceProvider)
{
this.articleService =
serviceProvider.GetRequiredService<IArticleService>();
}
Prefer:
public View(IArticleService articleService)
{
this.articleService = articleService;
}
IServiceProvider should normally appear only in infrastructure code that must create a service selected dynamically at runtime, not in regular module, application, or domain classes.
Constructor best practices
Use one public constructor
A module control should normally have exactly one public constructor:
public View(
IArticleService articleService,
IPermissionService permissionService,
ILogger<View> logger)
{
this.articleService = articleService;
this.permissionService = permissionService;
this.logger = logger;
}
Avoid keeping a parameterless constructor for compatibility. Multiple constructors can make activator selection ambiguous and may allow the module to be created without its required dependencies.
Keep constructors simple
The constructor should only:
- Validate constructor arguments.
- Assign dependencies to fields.
- Establish simple object invariants.
Do not perform database queries, logging workflows, redirects, or other request processing in the constructor.
DNN assigns module-specific context after constructing the control. Properties such as ModuleConfiguration, ModuleId, TabId, and other request-related state should therefore be used during the WebForms lifecycle, such as OnInit, Page_Load, or later—not in the constructor.
Prefer explicit dependencies
Inject the narrowest useful abstraction:
public View(IArticleService articleService)
Avoid broad dependencies that expose unrelated functionality:
public View(IServiceProvider serviceProvider)
public View(IDnnContext contextContainingEverything)
A large constructor often indicates that the control has too many responsibilities. Move business logic into focused application services instead of hiding dependencies behind a service locator.
Choose the correct service lifetime
Use the normal Microsoft dependency injection lifetimes:
services.AddTransient<IValueFormatter, ValueFormatter>();
services.AddScoped<IArticleService, ArticleService>();
services.AddSingleton<IApplicationCache, ApplicationCache>();
Transient
Use Transient for lightweight, stateless services that can be created each time they are requested.
Scoped
Use Scoped for services whose lifetime belongs to the current web request. This is usually the appropriate default for application services, repositories, and request-related operations.
DNN resolves WebForms constructor dependencies from the current request scope.
Singleton
Use Singleton only for thread-safe services that do not depend on request, portal, user, module, HTTP context, or scoped services.
A singleton must not capture a scoped service:
// Invalid lifetime relationship:
services.AddScoped<IArticleRepository, ArticleRepository>();
services.AddSingleton<IArticleCache, ArticleCache>();
If ArticleCache depends on IArticleRepository, the scoped repository may effectively be retained for the lifetime of the application.
Testing
Constructor injection allows dependencies to be supplied directly in tests without configuring DNN’s global service provider.
[Test]
public void View_UsesInjectedArticleService()
{
var articleService = new FakeArticleService();
var view = new View(articleService);
// Exercise the relevant behavior.
}
Business behavior should preferably live in independently testable services, leaving the WebForms control responsible for adapting WebForms and DNN lifecycle events to those services.
Tests should not need to modify:
Globals.DependencyProvider
Changing global DI state makes tests order-dependent and can cause state to leak between tests.
Extension points that are not WebForms controls
HttpRuntime.WebObjectActivator applies to objects created through the ASP.NET WebForms activation pipeline. It does not automatically add constructor injection to every legacy DNN extension point.
Schedulers, search providers, business controllers, portable interfaces, navigation providers, Prompt commands, connectors, and similar extension points use their own DNN activation pipelines. Constructor injection is available only when that specific pipeline creates instances through DNN’s DI container.
Do not assume that an arbitrary type named in a DNN manifest supports constructor injection. Check the API for that extension point.
Do not replace DNN’s WebObjectActivator
DNN 10 owns the application-level registration of:
HttpRuntime.WebObjectActivator
A module must not replace it with its own container adapter. Doing so affects every WebForms object in the application and can break DNN or other extensions.
Modules should integrate with the existing DNN container exclusively through IDnnStartup.
Migration from the service-locator pattern
Before:
public partial class View : PortalModuleBase
{
private readonly IArticleService articleService;
public View()
{
this.articleService =
this.DependencyProvider.GetRequiredService<IArticleService>();
}
}
After:
public partial class View : PortalModuleBase
{
private readonly IArticleService articleService;
public View(IArticleService articleService)
{
this.articleService = articleService;
}
}
Migration steps:
- Identify every call to
DependencyProvider.GetService, GetRequiredService, Globals.DependencyProvider, or another global resolver.
- Add the resolved type as an explicit constructor parameter.
- Store it in a
readonly field.
- Ensure the dependency and its complete dependency graph are registered in
IDnnStartup.
- Remove the service-provider access from the class.
- Verify that the selected service lifetimes are compatible.
- Add an isolated unit test that constructs the class with test implementations.
Troubleshooting
Unable to resolve service
An exception similar to the following means the dependency or one of its transitive dependencies is not registered:
Unable to resolve service for type 'IArticleService'
while attempting to activate 'View'.
Check the complete dependency chain, not only the first constructor.
Constructor is not used
Verify that:
- The site is running DNN 10 or later.
- The control and its constructor are public.
- The control is created by the WebForms pipeline.
- The module has not replaced
HttpRuntime.WebObjectActivator.
- The deployed assembly contains the current implementation.
Startup registration is not executed
Verify that:
Startup implements IDnnStartup.
- The class is concrete and has a public parameterless constructor.
- The assembly is deployed to the site’s
bin directory.
- No exception is thrown from
ConfigureServices.
- The application has restarted after deployment.
Summary
For WebForms modules on DNN 10 and later:
- Register services through
IDnnStartup.
- Declare module dependencies as constructor parameters.
- Use a single public constructor.
- Resolve dependencies from the current request scope through DNN’s built-in activation pipeline.
- Keep request and module context out of constructors.
- Do not call
DependencyProvider.GetService from application code.
- Do not inject
IServiceProvider merely to resolve other services.
- Do not replace DNN’s
HttpRuntime.WebObjectActivator.
References: