DNN dependency injection

Why and How

Brian Dukes

Engage

@bdukes@mastodon.cloud

What is dependency injection?

Control Without Inversion

[DnnModuleAuthorize]
[SupportedModules("Engage: Sync")]
public class SyncController : DnnApiController
{
  [HttpPost]
  public async Task<HttpResponseMessage> Sync()
  {
    using var context = new SyncContext();
    var service = new SyncService(context);
    var result = await service.Sync();
    return this.Request.CreateResponse(
      HttpStatusCode.OK,
      new { status = result.Status, count = result.Count });
  }
}
# new Dependency()

Inversion of Control

[DnnModuleAuthorize]
[SupportedModules("Engage: Sync")]
public class SyncController : DnnApiController
{
  private readonly ISyncService syncService;

  public SyncController(ISyncService syncService)
  {
    this.syncService = syncService;
  }

  [HttpPost]
  public async Task<HttpResponseMessage> Sync()
  {
    var result = await this.syncService.Sync();
    return this.Request.CreateResponse(
      HttpStatusCode.OK,
      new { status = result.Status, count = result.Count });
  }
}
# Constructor

Property Injection

public class MenuPermissionAttribute : AuthorizeAttributeBase, IOverrideDefaultAuthLevel
{
  public ServiceScope Scope { get; set; }
  public string MenuName { get; set; }
  public string Exclude { get; set; }

  [Dependency]
  private IPersonaBarController PersonaBarController { get; set; }

  /// <inheritdoc/>
  public override bool IsAuthorized(AuthFilterContext context)
  {
    var authenticated = Thread.CurrentPrincipal.Identity.IsAuthenticated;
    var portalSettings = PortalSettings.Current;
    var currentUser = UserController.Instance.GetCurrentUserInfo();

    var isAdmin = currentUser.IsInRole(portalSettings?.AdministratorRoleName ?? Constants.AdminsRoleName);

    if (authenticated && currentUser.IsSuperUser)
    {
      return true;
    }

    var menuItem = this.GetMenuByIdentifier(this.MenuName);
    if (menuItem != null && portalSettings != null)
    {
    	return this.PersonaBarController.IsVisible(portalSettings, portalSettings.UserInfo, menuItem);
    }

    switch (this.Scope)
    {
      case ServiceScope.Admin:
      	return authenticated && isAdmin;
      case ServiceScope.Regular:
        if (portalSettings != null)
        {
          // if user have ability on any persona bar menus, then need allow to request api.
          return this.PersonaBarController.GetMenu(portalSettings, portalSettings.UserInfo).AllItems.Count > 0;
        }

        return isAdmin || currentUser.UserID > 0;
      default:
      	return false;
      }
    }
  }
}
# Dependency Attribute

Why dependency injection?

Benefits

Lifetime

Consuming code is not in control of the lifetime of dependencies

1.

2.

Abstraction

A focus on abstractions can result in loosely coupled code

3.

Framework

It's how DNN (and .NET) manages and exposes its own dependencies

# Why

4.

Central Management

One place to manage abstractions and lifetimes

Where is injection supported?

MVC Controller (DNN 9.4.0)

using DotNetNuke.Web.Mvc.Framework.Controllers;

[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Anonymous)]
[DnnHandleError]
public class HomeController : DnnController
{
  private readonly IMessageService messageService;

  public HomeController(IMessageService messageService)
  {
    this.messageService = messageService;
  }

  public ActionResult Index()
  {
    var model = new HelloWorld
      {
        Message = this.messageService.GetMessage(),
      };
    return View(model);
  }
}
# Introducing DI

Web API Controller (DNN 9.4.0)

using DotNetNuke.Web.Api;

[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.View)]
public class HomeController : DnnApiController
{
  private readonly IMessageService messageService;

  public HomeController(IMessageService messageService)
  {
    this.messageService = messageService;
  }

  [HttpGet]
  public string Get()
  {
    return this.messageService.GetMessage();
  }
}
# Introducing DI

Razor Model (DNN 9.4.0)

public class IndexModel
{
  private readonly IMessageService messageService;

  public IndexModel(IMessageService messageService)
  {
    this.messageService = messageService;
  }

  public string Title => this.messageService.GetMessage();
}
# Introducing DI

WebForms Module (DNN 9.4.0)

using Microsoft.Extensions.DependencyInjection;
using DotNetNuke.Entities.Modules;

public partial class View : PortalModuleBase
{
  private readonly IMessageService messageService;

  public View()
  {
    this.messageService = this.DependencyProvider.GetRequiredService<IMessageService>();
  }
  
  protected override void OnInit(EventArgs e)
  {
    this.Message.Text = this.messageService.GetMessage();
  }
}
# Introducing DI

Scheduled Task (DNN 9.6.1)

using DotNetNuke.Services.Scheduling;

public class ForceResetPasswordTask : SchedulerClient
{
    private readonly ResetPasswordService service;

    public ForceResetPasswordTask(ScheduleHistoryItem item, ResetPasswordService service)
    {
        this.service = service;
        this.ScheduleHistoryItem = item;
    }

    public override void DoWork()
    {
        try
        {
            var result = AsyncHelper.Run(this.service.ForcePasswordResetAsync);
            this.ScheduleHistoryItem.AddLogNote(result);
            this.ScheduleHistoryItem.Succeeded = true;
        }
        catch (Exception exception)
        {
            this.ScheduleHistoryItem.Succeeded = false;
            this.ScheduleHistoryItem.AddLogNote($"Exception while forcing password resets: {exception.Message}");
            this.Errored(ref exception);
            Exceptions.LogException(exception);
        }
    }
}
# More DI

Service Route Mapper (DNN 9.8.0)

using DotNetNuke.Web.Api;

public class ExampleRouteMapper : IServiceRouteMapper
{
  private readonly IRouteSource routeSource;
  
  public ExampleRouteMapper(IRouteSource routeSource)
  {
    this.routeSource = routeSource;
  }

  public void RegisterRoutes(IMapRoute mapRouteManager)
  {
    foreach (var route = this.routeSource.GetRoutes())
    {
      mapRouteManager.MapHttpRoute(
        route.BasePath,
        route.Name,
        route.Template,
        new[] { route.Namespace, });
    }
  }
}
# Web API

Web API Action Filter (DNN 9.9.0)

public class MenuPermissionAttribute : AuthorizeAttributeBase, IOverrideDefaultAuthLevel
{
  [Dependency]
  private IPersonaBarController PersonaBarController { get; set; }

  /// <inheritdoc/>
  public override bool IsAuthorized(AuthFilterContext context)
  {
    return this.PersonaBarController.IsVisible(context);
  }
}
# Web API
  • WebForms Constructor Injection
  • Business Controller Class
    • IPortable
    • IUpgradeable
    • ModuleSearchBase

🆕 in DNN 10

  • Module Injection Filter
  • Event Message Processor
  • Persona Bar Extension Controller
  • Persona Bar Menu Controller
  • Site Import/Export
  • DDR Menu Localization
  • Navigation Provider
  • Auth Message Handler
  • Image Transform
  • Connectors
  • Prompt Commands
  • more?

🆕 in DNN 10

What can be injected?

Choose Your Own Dependency

using DotNetNuke.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

public class Startup : IDnnStartup
{
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddSingleton<BaseClass, ExpensiveThing>();
    services.AddScoped<StatefulThing>();
    services.AddTransient<IThing, RegularThing>();
    services.TryAddTransient(_ => PortalController.Instance);
  }
}
# Startup

DNN 9.4.2

namespace DotNetNuke.Abstractions
{
  public interface INavigationManager
  {
    string NavigateURL();
    string NavigateURL(int tabID);
    string NavigateURL(int tabID, bool isSuperTab);
    string NavigateURL(string controlKey);
    string NavigateURL(string controlKey, params string[] additionalParameters);
    string NavigateURL(int tabID, string controlKey);
    string NavigateURL(int tabID, string controlKey, params string[] additionalParameters);
    string NavigateURL(int tabID, IPortalSettings settings, string controlKey, params string[] additionalParameters);
    string NavigateURL(int tabID, bool isSuperTab, IPortalSettings settings, string controlKey, params string[] additionalParameters);
    string NavigateURL(int tabID, bool isSuperTab, IPortalSettings settings, string controlKey, string language, params string[] additionalParameters);
    string NavigateURL(int tabID, bool isSuperTab, IPortalSettings settings, string controlKey, string language, string pageName, params string[] additionalParameters);
  }
}
# Built-in

DNN 9.7.1

namespace DotNetNuke.Abstractions.Application
{
  public interface IDnnContext
  {
    IApplicationInfo Application { get; }
  }
  public interface IApplicationInfo
  {
    string Company { get; }
    Version CurrentVersion { get; }
    string Description { get; }
    string HelpUrl { get; }
    string LegalCopyright { get; }
    string Name { get; }
    string SKU { get; }
    ReleaseMode Status { get; }
    string Title { get; }
    string Trademark { get; }
    string Type { get; }
    string UpgradeUrl { get; }
    string Url { get; }
    Version Version { get; }
    bool ApplyToProduct(string productNames);
  }
  public interface IApplicationStatusInfo
  {
    UpgradeStatus Status { get; }
    string ApplicationMapPath { get; }
    Version DatabaseVersion { get; }
    bool IsInstalled();
    void SetStatus(UpgradeStatus status);
    void UpdateDatabaseVersion(Version version);
    void UpdateDatabaseVersionIncrement(Version version, int increment);
    bool IncrementalVersionExists(Version version);
    int GetLastAppliedIteration(Version version);
  }
  public interface IHostSettingsService
  {
    bool GetBoolean(string key);
    bool GetBoolean(string key, bool defaultValue);
    double GetDouble(string key);
    double GetDouble(string key, double defaultValue);
    string GetEncryptedString(string key, string passPhrase);
    int GetInteger(string key);
    int GetInteger(string key, int defaultValue);
    IDictionary<string, IConfigurationSetting> GetSettings();
    IDictionary<string, string> GetSettingsDictionary();
    string GetString(string key);
    string GetString(string key, string defaultValue);
    void IncrementCrmVersion(bool includeOverridingPortals);
    void Update(IConfigurationSetting config);
    void Update(IConfigurationSetting config, bool clearCache);
    void Update(IDictionary<string, string> settings);
    void Update(string key, string value);
    void Update(string key, string value, bool clearCache);
    void UpdateEncryptedString(string key, string value, string passPhrase);
  }
}
# Built-in

DNN 9.7.2

namespace DotNetNuke.Abstractions.Portals
{
  public interface IPortalAliasService
  {
    string GetPortalAliasByPortal(int portalId, string portalAlias);
    string GetPortalAliasByTab(int tabId, string portalAlias);
    bool ValidateAlias(string portalAlias, bool ischild);
    int AddPortalAlias(IPortalAliasInfo portalAlias);
    void DeletePortalAlias(IPortalAliasInfo portalAlias);
    IPortalAliasInfo GetPortalAlias(string alias);
    IPortalAliasInfo GetPortalAlias(string alias, int portalId);
    IPortalAliasInfo GetPortalAliasByPortalAliasId(int portalAliasId);
    IDictionary<string, IPortalAliasInfo> GetPortalAliases();
    IEnumerable<IPortalAliasInfo> GetPortalAliasesByPortalId(int portalId);
    IPortalInfo GetPortalByPortalAliasId(int portalAliasId);
    void UpdatePortalAlias(IPortalAliasInfo portalAlias);
  }
}
# Built-in

DNN 9.8.0

namespace DotNetNuke.Abstractions
{
  public interface ISerializationManager
  {
    string SerializeValue<T>(T value);
    string SerializeValue<T>(T value, string serializer);
    string SerializeProperty<T>(T myObject, PropertyInfo property);
    string SerializeProperty<T>(T myObject, PropertyInfo property, string serializer);
    T DeserializeValue<T>(string value);
    T DeserializeValue<T>(string value, string serializer);
    void DeserializeProperty<T>(T myObject, PropertyInfo property, string propertyValue)
      where T : class, new();
    void DeserializeProperty<T>(T myObject, PropertyInfo property, string propertyValue, string serializer)
      where T : class, new();
  }
}
namespace DotNetNuke.Abstractions.Logging
{
  public interface IEventLogger
  {
    void AddLog(string name, string value, EventLogType logType);
    void AddLog(string name, string value, IPortalSettings portalSettings, int userID, EventLogType logType);
    void AddLog(string name, string value, IPortalSettings portalSettings, int userID, string logType);
    void AddLog(ILogProperties properties, IPortalSettings portalSettings, int userID, string logTypeKey, bool bypassBuffering);
    void AddLog(IPortalSettings portalSettings, int userID, EventLogType logType);
    void AddLog(object businessObject, IPortalSettings portalSettings, int userID, string userName, EventLogType logType);
    void AddLog(object businessObject, IPortalSettings portalSettings, int userID, string userName, string logType);
    void AddLog(ILogInfo logInfo);
  }
  public interface IEventLogConfigService
  {
    void AddLogType(string configFile, string fallbackConfigFile);
    void AddLogTypeConfigInfo(ILogTypeConfigInfo logTypeConfig);
    void AddLogType(ILogTypeInfo logType);
    void DeleteLogType(ILogTypeInfo logType);
    void DeleteLogTypeConfigInfo(ILogTypeConfigInfo logTypeConfig);
    IEnumerable<ILogTypeConfigInfo> GetLogTypeConfigInfo();
    ILogTypeConfigInfo GetLogTypeConfigInfoByID(string id);
    IDictionary<string, ILogTypeInfo> GetLogTypeInfoDictionary();
    void UpdateLogTypeConfigInfo(ILogTypeConfigInfo logTypeConfig);
    void UpdateLogType(ILogTypeInfo logType);
  }
  public interface IEventLogService : IEventLogger
  {
    void ClearLog();
    void DeleteLog(ILogInfo logInfo);
    IEnumerable<ILogInfo> GetLogs(int portalID, string logType, int pageSize, int pageIndex, ref int totalRecords);
    ILogInfo GetLog(string logGuid);
    void PurgeLogBuffer();
  }
}
# Built-in

DNN 9.11.1

namespace DotNetNuke.Abstractions.Portals.Templates
{
  public interface IPortalTemplateController
  {
    void ApplyPortalTemplate(int portalId, IPortalTemplateInfo template, int administratorId, PortalTemplateModuleAction mergeTabs, bool isNewPortal);
    (bool success, string message) ExportPortalTemplate(int portalId, string fileName, string description, bool isMultiLanguage, IEnumerable<string> locales, string localizationCulture, IEnumerable<int> exportTabIds, bool includeContent, bool includeFiles, bool includeModules, bool includeProfile, bool includeRoles);
    IPortalTemplateInfo GetPortalTemplate(string templatePath, string cultureCode);
    IList<IPortalTemplateInfo> GetPortalTemplates();
  }
}
# Built-in

Tips & Tricks

Prompt Command

In the Prompt page of the Persona Bar, run list-services to see what's registered

1.

2.

HTTP Client & Polly

Add

Microsoft. Extensions.Http. Polly and automatically handle transient errors

3.

Get Request Scope

Use the service scope from the current HTTP request to do service location where DI isn't yet supported

# Bonus

list-services

# Prompt

HttpClient & Polly

# Error Handling
using Polly;
using Polly.Contrib.WaitAndRetry;
using Polly.Extensions.Http;
using Polly.Registry;

public static class Policies
{
    public const string RetryWithExponentialBackoff = nameof(RetryWithExponentialBackoff);
    private const string LoggerContextKey = "logger";

    public static void InitializeRegistry([NotNull]IPolicyRegistry<string> pollyRegistry)
    {
        if (pollyRegistry == null)
        {
            throw new ArgumentNullException(nameof(pollyRegistry));
        }

        pollyRegistry.Add(
            RetryWithExponentialBackoff,
            HttpPolicyExtensions.HandleTransientHttpError()
                                .WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), 4), OnRetry));
    }

    public static Context PrepareContext(string operationKey, ILogger logger)
    {
        return new Context(operationKey, new Dictionary<string, object>(1) { { LoggerContextKey, logger }, });
    }

    private static void OnRetry(DelegateResult<HttpResponseMessage> result, TimeSpan wait, int retryCount, Context context)
    {
        if (!context.TryGetValue(LoggerContextKey, out var loggerObject) || loggerObject is not ILogger logger)
        {
            return;
        }

        if (result.Exception is not null)
        {
            logger.LogRetryOnException(result.Exception, wait, retryCount);
        }
        else
        {
            logger.LogRetryOnTransientHttpError(result.Result, wait, retryCount);
        }
    }
}

using DotNetNuke.DependencyInjection;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

public class Startup : IDnnStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
        var pollyRegistry = services.AddPolicyRegistry();
        Policies.InitializeRegistry(pollyRegistry);

        services.AddHttpClient<SearchService>(ConfigureSearchService).AddPolicyHandlerFromRegistry(Policies.RetryWithExponentialBackoff);
    }

    private static void ConfigureSearchService(IServiceProvider serviceProvider, HttpClient client)
    {
        client.BaseAddress = new Uri(Config.GetSettings("SearchServiceBaseUrl"));
    }
}

HTTP Request Scope

# Skins & friends
<script runat="server">
private string GetJson(object someObject)
{
  var httpContext = HttpContextSource.Current;
  var serviceScope = httpContext.GetScope();
  var jsonSvc = serviceScope.ServiceProvider.GetService<IJsonService>();
  return jsonSvc.ToJson(someObject);
}
</script>

<div class="json-array"><%: GetJson(new[] { 1, 2, 3, }) %></div>

DNN Dependency Injection

By Brian Dukes

DNN Dependency Injection

  • 386