Brian Dukes
Work at Engage Software, developing websites on the DNN Platform (a DNN MVP). I also serve Jesus at City Lights Church.
Why and How
Brian Dukes
Engage
@bdukes@mastodon.cloud
[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()
[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
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
Lifetime
Consuming code is not in control of the lifetime of dependencies
Abstraction
A focus on abstractions can result in loosely coupled code
Framework
It's how DNN (and .NET) manages and exposes its own dependencies
# Why
Central Management
One place to manage abstractions and lifetimes
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
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
public class IndexModel
{
private readonly IMessageService messageService;
public IndexModel(IMessageService messageService)
{
this.messageService = messageService;
}
public string Title => this.messageService.GetMessage();
}
# Introducing DI
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
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
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
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
IPortable
IUpgradeable
ModuleSearchBase
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
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
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
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
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
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
Prompt Command
In the Prompt page of the Persona Bar, run list-services
to see what's registered
HTTP Client & Polly
Add
Microsoft. Extensions.Http. Polly
and automatically handle transient errors
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"));
}
}
# 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>
By Brian Dukes
Work at Engage Software, developing websites on the DNN Platform (a DNN MVP). I also serve Jesus at City Lights Church.