Table of Contents

Custom C# Web API: Hybrid Dnn & Oqtane APIs

Dnn and Oqtane have a few differences because of these important factors:

  1. Different underlying C# and .net frameworks
  2. Different platforms (Dnn is different from Oqtane)
  3. Different security attributes for each platform
Important

This is very advanced stuff and only relevant if you want to create Apps which run on both Dnn and Oqtane.

For most of your apps this is not relevant

2sxc Philosophy - Stay out of the Way

Our philosophy is to not reinvent the wheel so it's important that we let you use the .net APIs as they are designed. Se we don't plan on creating another API which to hide the differences, but let you understand them and easily handle everything as needed.

Core Strategies for Hybrid WebAPIs

If you follow these three rules you should be good to go:

  1. Inherit from Custom.Hybrid.Api12
  2. Use very common C# features and syntaxes which existed since C# 7.2 (so anything that runs in Dnn will also run in Oqtane)
  3. Use .net standard 2.0 APIs and avoid using System.Web
  4. Where necessary, use preprocessor directives as explained below

The Preprocessor Directives

C# has special #if preprocessor statements which are evaluated during compilation. Using this you can define which code should be used in Dnn and Oqtane. Here's an example:

// Add namespaces to enable security in Oqtane & Dnn despite the differences
#if OQTANE
using Microsoft.AspNetCore.Authorization; // .net core [AllowAnonymous] & [Authorize]
using Microsoft.AspNetCore.Mvc;           // .net core [HttpGet] / [HttpPost] etc.
using Oqtane.Shared;        // Oqtane role names
#else
using System.Web.Http;      // .net 4.5 [AllowAnonymous] / [HttpGet]
using DotNetNuke.Web.Api;   // [DnnModuleAuthorize]
using DotNetNuke.Security;  // SecurityAccessLevel.Xyz
#endif

// All commands can be accessed without security checks
public class HybridController : ToSic.Custom.Api12
{
  [AllowAnonymous]  // Works in Oqtane and Dnn
  [HttpGet]         // Works in Oqtane and Dnn
  public string Hello()
  {
    return "Hello from the basic controller in /api";
  }

  #if OQTANE
  [Authorize(Roles = RoleNames.Everyone)]
  #else
  [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Anonymous)]
  #endif
  [HttpGet]
  public int Square(int number)
  {
    return number * number;
  }
}

The following symbols are set when Api Controllers are compiled:

Key True for Dnn True for Oqtane Comments
NETCOREAPP Only works in WebAPIs and code (.cs)
OQTANE Only works in WebAPIs and code (.cs)

Important: These don't work in Razor on Oqtane 3+ because we cannot activate these in the compiler - so they only work in .cs files

Use like this:

  • #if OQTANE ... #endif
  • #if OQTANE ... #else ... #endif
  • #if !OQTANE ... #endif
  • #if !OQTANE ... #else ... #endif

You can't use #if Dnn ... #endif because of limitations in the dynamic C# compiler of Dnn. Just use #if !OQTANE ... #endif.

Different C# and .net Frameworks

Part Dnn 7 Dnn 9 Oqtane
C# Language ca. 7 ca. 7 C# 9
.net Framework 4.5.1 4.7.2 .net core 5
.net Standard 1.6 2.0 2.0

Any hybrid controller must limit itself to features in .net standard 1.6 or 2.0, depending on the platforms you want to support. Note that any 2sxc standard apps are meant to still run in Dnn 4.7.2, so we'll restrict our work to support .net standard 1.6. This means our examples are more limited than what you will be doing.

Differences in the Platforms

If you are creating hybrid controllers, we'll assume that you usually don't need to access properties of Dnn or Oqtane. If you do, you'll have to use the standard mechanisms provided by these.

  • In Dnn - use global objects like PortalSettings.Current
  • In Oqtane use Dependency Injection
  • To avoid the code from causing trouble during compilation, wrap the necessary differences in #if OQTANE ... #endif and #if !OQTANE ... #endif blocks

Security Attribute Differences

All APIs need to have the correct attributes to mark the security requirements.

// Oqtane way
[Authorize(Roles = RoleNames.Admin)]

// Dnn way
[DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Admin)]

The Attributes come from these namespaces:

  • Dnn: DotNetNuke.Web.Api
    • [DnnModuleAuthorize] - the most common security attribute
    • [DnnAuthorize] - for special cases, we're not sure if it's ever used.
      my understanding is that it's for protecting an endpoint that will be accessed by users, but not from a module on the page, so the module-context would be missing. note that it's very different from the DnnModuleAuthorize
    • [SupportedModules] - to limit access to an API from specific modules. You probabably won't ever use this.
  • Oqtane: Microsoft.AspNetCore.Authorization
    • [Authorize] - standard Authorize of .net core

The list of possible values come from these namespaces/enums/constants:

  • Dnn: DotNetNuke.Security.SecurityAccessLevel
    Example: #todoc
  • Oqtane Core Roles Oqtane.Shared.RoleNames
    Example: [Authorize(Roles = RoleNames.Host)] Multiple: [Authorize(Roles = RoleNames.Host, RoleNames.Admin)]
  • Oqtane Common Permissions: Oqtane.Shared.PolicyNames
    Example: [Authorize(Policy = PolicyNames.ViewPage)] Multiple: [Authorize(Policy = PolicyNames.ViewPage, PolicyNames.ViewFolder)]

Permissions of this using DnnModuleAuthorize or Authorize

Target Permission DnnAuthorize DnnModuleAuthorize Oqtane Authorize
System Admin ? SecurityAccessLevel.Host RoleNames.Host
Site Admin ? SecurityAccessLevel.Admin RoleNames.Admin
Registered users ? ? RoleNames.Registered
Anybody ? Anonymous RoleNames.Everyone
Module Editor ? SecurityAccessLevel.Edit PolicyNames.EditModule
PolicyNames.EditPage
Module Viewer ? SecurityAccessLevel.View PolicyNames.ViewModule
PolicyNames.ViewPage
Module Permissions Manager ? SecurityAccessLevel.ViewPermissions RoleNames.Admin
Module ControlPanel ? SecurityAccessLevel.ControlPanel RoleNames.Admin
Module SkinObjects ? ? SecurityAccessLevel.SkinObject RoleNames.Admin

ValidateAntiForgeryToken Differences

Comparison

Purpose Attribute Dnn Namespace Oqtane/.net Core Namespace
Enforce Anti-Forgery [ValidateAntiForgeryToken] DotNetNuke.Web.Api Microsoft.AspNetCore.Mvc
Skip Enforcing on a method even if Class enforces it [IgnoreAntiforgeryToken] (doesn't exist) Microsoft.AspNetCore.Mvc
Auto Enforce on unsafe calls (POST, etc.) [AutoValidateAntiforgeryToken] (doesn't exist) Microsoft.AspNetCore.Mvc

Sidenote: in the .net core implementation the attributes can also accept an order parameter which defaults to 1000. We believe you should never change this :) but it does allow for other security configurations than were possible in Dnn.

HTTP Verbs like GET, POST

All APIs need to have attributes like [HttpGet] and [HttpPost]. The main difference you need to be aware of is that they must come from a different namespace. Use the #if ... statements as shown in the example above.

API Result JSON Output

Standard Implementations:

  • In Dnn WebApis all data is automatically converted to JSON. This was an early design decision of 2sxc and works great for most cases, but some edge cases (like responding with XML) is more difficult this way.
  • In Oqtane (.net core 5) the default is more sophisticated. The methods return objects or values. In advanced cases they will return an ActionResult or ContentResult. The default encoding is as follows:
    • Simple values like strings are returned just as-is
    • Complex objects are serialized - by default as json

So for anything more complex the behavior is often identical, but for simple values it's different unless you specify explicitly what you want:

Value Type Dnn 2sxc API Oqtane Apis Comments
27 int 27 27 identical
"Hello World" string "Hello World" Hello World Note missing quotes in Oqtane which makes this non-json
["a", "b"] string[] ["a", "b"] ["a", "b"] identical

The most important difference is that by default, strings are not converted to JSON

If you need to return a simple string and must ensure it's JSON on both platforms, add this attribute to your class or method:

[Produces("application/json")]

This is in the namespace using Microsoft.AspNetCore.Mvc which you usually already have. Since Dnn won't know it, you will probably wrap it in an #if OQTANE like this:

#if OQTANE
[Produces("application/json")]
#endif

History

  1. Introduced in 2sxc 12.00