Add an API controller

Contents[Hide]

1. Overview

This article shows how to add a custom API controller to Dundas BI.

2. Getting started

For Dundas BI version 10 and higher, the following prerequisites must be installed on your computer to build the provided sample without modifications:

  • Visual Studio 2022 or higher
  • Microsoft .NET 6

For Dundas BI version 9 and earlier installed on Windows, the following prerequisites must be installed on your computer to build the provided sample without modifications:

  • Visual Studio 2017 or higher
  • Microsoft .NET Framework 4.7.2

Note
Microsoft .NET Framework versions 4 and earlier run only on Windows. To add a custom API controller on other platforms, upgrade to Dundas BI version 10.

2.1. Downloading sample solution

To download the custom API controller sample solution, click on the link below for your version of Dundas BI:

(A solution is also available for Dundas BI version 6.)

2.2. Opening solution

Extract CustomApiController.zip to a folder and open the solution in Microsoft Visual Studio to build the extension.

To use the option to publish directly to your Dundas BI instance, run Visual Studio as an administrator before opening the solution. For example, you can right-click Visual Studio in the start menu and find the Run as administrator option.

The solution file is located at:
[Extracted folder]\CustomApiController\CustomApiController.sln

3. The project

The project is a class library.

  • CustomApiControllerPackage.cs - This class contains the package information about the extension package.
  • HelloWorldController.cs - Class that defines the API controller.
  • PublishExtensionTemplate.props - Used for auto publishing the extension after the build succeeds, and defines extension properties, and files.

3.1. ExtensionPackageInfo class

        

/// <summary>
/// This class contains the package information about the extension package.
/// </summary>
public class CustomApiControllerPackage : ExtensionPackageInfo2
{
    /// <summary>Initializes a new instance of the 
    /// <see cref="CustomApiControllerPackage"/> 
    /// class.</summary>
    /// <param name="extensionManifest">The extension manifest.</param>
    public CustomApiControllerPackage(ExtensionManifest extensionManifest)
        : base(extensionManifest)
    {
    }
}

3.2. Publish extension template

This sample has a mechanism to automatically publish the extension when building, which is the Dundas.BI.PublishExtension NuGet package. When this package is added to the project, it creates a PublishExtensionTemplate.props file containing MSBuild property and item groups, which define how to create and publish the extension.

When the DtFilePath property is set to the file path of the dt tool of a Dundas BI instance, it will then publish the extension directly to that instance when you build the solution. It will also touch the web.config file to force the web application to reset.

If the DtFilePath property is not set, it will create a .zip file you can add to your Dundas BI instance using the Extensions screen in the administration UI. After building the solution, this .zip file can be found within the bin subfolder of your solution.

For more details on using this package to automate publishing extensions, see Using the Dundas.BI.PublishExtension NuGet package.

3.3. Implementing the abstract controller class

An API controller will need to derive from the ControllerBase class in .NET 6 (ApiController in .NET Framework). The derived class will be picked up automatically, and a route will be established for the one method defined below to:

POST https://{DundasBIUrl}/API/HelloWorld
[Route("api/HelloWorld")]
[ApiController]
public class HelloWorldController : ControllerBase
{
    public HelloWorldController() 
    {
    }

    [HttpPost]
    public IActionResult Post([FromBody]dynamic options, Guid? sessionId = null)
    {
        // ...
    }
}

Tip
For more information about API controllers in ASP.NET 6, see Create web APIs with ASP.NET Core. (For web API routes in .NET Framework instead, see Routing in ASP.NET Web API.)

3.4. Creating a full hello world example

The following example will create a route for:

POST https://{DundasBIUrl}/API/HelloWorld

This takes a JSON argument in the body which is defined as a dynamic type named options in the method. The options contain a property called withUsername and the reply will contain either Hello {username}, or Hello! based on that property:

using System;
using System.Globalization;

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;

using Dundas.BI;
using Dundas.BI.AccountServices;

/// <summary>
/// A basic API controller extension sample.
/// </summary>
[Route("api/HelloWorld")]
[ApiController]
public class HelloWorldController : ControllerBase
{
    #region Constructors

    /// <summary>
    /// Initializes a new instance of the <see cref="HelloWorldController"/> class.
    /// </summary>
    public HelloWorldController() 
    {
    }

    #endregion Constructors

    #region Private Methods

	private Guid? GetSessionIdFromCookieOrProvided(Guid? sessionIdFromQueryString)
	{
		// Check if it's provided in the query string.
		if (sessionIdFromQueryString.HasValue && sessionIdFromQueryString.Value != Guid.Empty)
		{
			return sessionIdFromQueryString;
		}

        // Check for an authorization header.
        string sessionString;
        StringValues authorizationHeader;
        if (this.Request.Headers.TryGetValue("Authorization", out authorizationHeader))
        {
            sessionString = authorizationHeader.ToString();
            sessionString = sessionString.Replace("Bearer", string.Empty, StringComparison.OrdinalIgnoreCase);
            sessionString = sessionString.Trim();
        }
        // Try to get it from the cookie.
        else if (!this.Request.Cookies.TryGetValue("dundas_webapp_sessionid", out sessionString))
        {
            return null;
        }

        if (Guid.TryParse(sessionString, out Guid sessionId) && sessionId != Guid.Empty)
        {
            return sessionId;
        }

        return null;
	}

	private CallerContext Session(Guid? sessionId)
	{
		ICallerContextService callerContextService = Engine.Current.GetService<ICallerContextService>();

		// If an empty GUID is passed, there is no session, so we need to pass null when creating the context.
		Guid? sessIdForContext = this.GetSessionIdFromCookieOrProvided(sessionId);

		// Store this session ID in case we need it for exception handling (it may not be the current session).
		this.StoreSessionIdInHttpContext(sessIdForContext);

		// Return the context.
		return callerContextService.CreateAndSetCurrentContext(sessIdForContext);
	}

	private void StoreSessionIdInHttpContext(Guid? sessionId)
	{
        // Used to associate exceptions with a session.
        try
        {
            if (this.HttpContext != null)
            {
                this.HttpContext.Items["sessionId"] = sessionId;
            }
        }
        catch (ObjectDisposedException)
        {
            // In case of unexpected error from framework
        }
    }

    #endregion Private Methods

    #region Public Methods

	/// <summary>
	/// <c>POST /API/HelloWorld/</c>
	/// Says hello.
	/// </summary>
	/// <param name="options">A JSON string containing the options for the transfer.</param>
	/// <param name="sessionId">The current session ID.</param>
	/// <returns>A <see cref="System.String"/> containing the hello world string.</returns>
	[HttpPost]
    public IActionResult Post([FromBody]dynamic options, Guid? sessionId = null)
	{
		// Breakpoint for testing for correct mapping.
		// System.Diagnostics.Debugger.Launch();

		if (!this.ModelState.IsValid || options == null)
		{
			return BadRequest(this.ModelState);
		}

		using (this.Session(sessionId))
		{
			try
			{
				Session session = Engine.Current.Session;

				if (options.withUsername == true)
				{
					return Ok(string.Format(
						CultureInfo.CurrentCulture,
						"Hello {0}!",
						session.AccountDisplayName
					));
				}
				else
				{
                    return Ok("Hello!");
				}
			}
			catch (ArgumentException ex)
			{
				return BadRequest(ex);
			}
			catch (InvalidOperationException ex)
			{
				return StatusCode(StatusCodes.Status405MethodNotAllowed, ex);
			}
			catch (NoPrivilegeException ex)
			{
				return StatusCode(StatusCodes.Status403Forbidden, ex);
			}
			catch (InvalidSessionException ex)
			{
				return StatusCode(Dundas.BI.WebApi.WebApiConstants.InvalidSessionHttpStatusCode, ex);
			}
		}
	}

    #endregion Public Methods
}

4. Making the route available

With Dundas BI installed on Windows, it may be necessary to stop the Dundas BI web application and delete the temporary internet files if you receive a message like No HTTP resource was found that matches the request URI ... . The files are located in a sub folder here: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files. After deleting the appropriate folder restart the Dundas BI web application. This will cause the application to create the routes again when it starts.

5. See also

Dundas Data Visualization, Inc.
400-15 Gervais Drive
Toronto, ON, Canada
M3C 1Y8

North America: 1.800.463.1492
International: 1.416.467.5100

Dundas Support Hours:
Phone: 9am-6pm, ET, Mon-Fri
Email: 7am-6pm, ET, Mon-Fri