Create a custom tenants provider

1. Overview

This sample shows how to create a custom tenants provider. A custom tenants provider allows for creating, and managing of multiple tenants from outside the Dundas BI application database. In this sample you will learn the following:

  • What is multi-tenancy
  • What is a custom tenants provider
  • How to get started
  • How to create a Dundas BI extension
  • How to create a tenant provider

2. What is multi-tenancy

Dundas BI has built-in support for multi-tenant deployment scenarios allowing you to easily create and manage tenants which are isolated from each other, add accounts for specific tenants and customize the license seats for each tenant individually.

For more information on multi-tenancy, click here.

3. What is a custom tenants provider

The custom tenant provider will be responsible for the following:

  • Creating, Managing, and storing of tenants.
  • Handling of data connector overrides.
  • Storing the user group information for each tenant. Each tenant has an administrators, and members group.
  • Storing the license seat information for each tenant.

Note
In the following example a simple CSV file is used as the backend for the custom tenant provider.

4. Getting started

The following prerequisites must be installed on your computer:

  • Visual Studio 2012, or higher.
  • Microsoft .NET Framework 4.5
  • Dundas BI Server

4.1. Downloading file

To download the Custom Tenants Provider Sample solution, click here.

4.2. Extracting sample to SDK folder

This sample is designed to automatically publish the extension to the instance. First you must extract the CustomTenantsProviderSample.zip to the SDK folder within the instance. To extract the CustomTenantsProviderSample.zip do the following:

  1. Find the SDK folder for your instance. It is located at [instance root]\sdk.
  2. Extract the CustomTenantsProviderSample.zip to the SDK folder.
  3. You should see the following result:

    Samples added to SDK folder
    Samples added to SDK folder

    With the following structure:

    [instance root]
        • sdk
            • Samples
                • CustomTenantsProviderSample

Important
If the sample is not in the exact folder, it will not work correctly.

4.3. Opening Solution

To open the Visual Studio solution simply do the following:

  1. Right click on the Microsoft Visual Studio shortcut, and click run as administrator.
  2. Click the File Menu, then Open Solution.
  3. Double click the solution located at: [instance root]\sdk\Samples\CustomTenantsProviderSample\CustomTenantsProviderSample.sln

5. The project

The project is a class library.

  • TenantInformation.csv - Comma separated values containing tenant information. This is used as the back end of the tenants provider. For this example we leave it blank until populated by Dundas BI.
  • CustomTenantsProvider.cs - This class contains the implementation of the custom tenant provider.
  • CustomTenantsProviderSamplePackageInfo.cs - This class contains the package information about the extension package.
  • PublishExtension.targets - Used for auto publishing the extension after the build succeeds. This also adds the TenantInformation.csv file to the application data folder.

5.1. ExtensionPackageInfo class

In order for a tenants provider to be read by Dundas BI it needs to first contain a class that extends the ExtensionPackageInfo class. This class contains the package information about the extension package.

/// <summary>
/// This class contains the package information about the extension package.
/// </summary>
public class CustomTenantsProviderSamplePackageInfo : ExtensionPackageInfo
{
	#region Public Properties
	/// <summary>
	/// Gets the name of the extension package author.
	/// </summary>
	public override string Author
	{
		get { return "Dundas Data Visualization Sample Author"; }
	}
	/// <summary>
	/// Gets the copyright text associated with the extension package.
	/// </summary>
	public override string Copyright
	{
		get { return "Dundas Data Visualization, Inc."; }
	}
	/// <summary>
	/// Gets the localized display name of the extension package.
	/// </summary>
	public override string DisplayName
	{
		get { return "Custom Tenants Provider Sample Package"; }
	}
	/// <summary>
	/// Gets the unique identifier of the extension package.
	/// </summary>
	public override Guid Id
	{
		get { return new Guid("0cdf181c-b489-400d-8901-c4cb76abc1d4"); }
	}
	/// <summary>
	/// Gets the name of the extension package.
	/// </summary>
	public override string Name
	{
		get {
			return "Custom Tenants Provider Sample Package"; 
                }
	}
	/// <summary>
	/// Gets the version of the extension package.
	/// </summary>
	public override Version Version
	{
		get { return new Version(0, 0, 1); }
	}
	#endregion Public Properties
}
        

5.2. Publish Extension Targets

This sample has a mechanism to automatically publish the extension, and copy the TenantInformation.csv file to the application data folder. This mechanism is the PublishExtension.targets file which overrides the AfterBuild target. This will create the following files after successfully compiling the solution:

    • [instance root]\www\BIWebsite\App_Data\Extensions\CustomTenantsProviderSample\bin\CustomTenantsProviderSample.dll

    • [instance root]\www\BIWebsite\App_Data\TenantInformation.csv

It will then touch the web.config to force the web application to reset.

5.3. Defining the custom provider

In order to create a custom tenant provider you will need to implement the ITenantsProvider Interface.

using Dundas.BI.AccountServices.Extensibility;
using Dundas.BI.AccountServices.MultiTenancy;
using Dundas.BI.Licensing;
using Dundas.BI.Utility;
using Newtonsoft.Json;
  ...
namespace Dundas.BI.Sample.CustomTenantsProviderSample
{
    /// <summary>
    /// This class represents a simple tenant provider that uses a csv file.
    /// </summary>
    public class CustomTenantsProvider : ITenantsProvider
    {
      ...
    }
}

5.4. Implementing the ITenantsProvider interface

5.4.1. Defining the supported query operations

The supported query operation specifies one or more operations available in the tenants provider. In the following example we return the Count operation. This means we should implement a System.NotSupportedException for the query method on the interface, and we should implement the QueryCount method.

/// <summary>
/// Gets the supported query operations.
/// </summary>
/// <value>
/// The supported query operations.
/// </value>
public Dundas.BI.Utility.QueryApiOperations SupportedQueryOperations
{
	get { return Dundas.BI.Utility.QueryApiOperations.Count; }
}
/// <summary>
/// Queries for tenant records.
/// </summary>
/// <param name="pageNumber">The page number.</param>
/// <param name="pageSize">The number of results in each page.</param>
/// <param name="orderBy">The sort order of the result, or <see langword="null" /> if the order does not matter.</param>
/// <param name="filter">The filter rules which should be applied to the query, or <see langword="null" /> if no filters are required.</param>
/// <returns>
/// The records matching the search criteria.
/// </returns>
/// <exception cref="System.NotSupportedException"></exception>
public IList<Guid> Query(int pageNumber, int pageSize, IList<Tuple<TenantQueryField, SortDirection>> orderBy, ICollection<TenantQueryFilterRule> filter)
{
	throw new NotSupportedException();
}
/// <summary>
/// Queries for the number of tenants matching a filter criteria.
/// </summary>
/// <param name="filter">The filter rules which should be applied to the query, or <see langword="null" /> if
/// the total number of tenants should be returned.</param>
/// <returns>
/// The number of tenants matching the filter.
/// </returns>
public int QueryCount(ICollection<TenantQueryFilterRule> filter)
{
	return GetAllRecords().Count();
}

5.4.2. Defining if modify is supported

The IsModifySupported property indicates whether the provider supports saving changes. If true all the methods that change the tenant should be implemented. Otherwise, they should throw a System.NotSupportedException.

This example demonstrates how to set the custom tenant provider to modify the tenants:

/// <summary>
/// Gets a value indicating whether the provider supports saving changes.
/// </summary>
public bool IsModifySupported
{
	get { return true; }
}

/// <summary>
/// Saves a tenant.
/// </summary>
/// <param name="tenant">The tenant to be saved.</param>
/// <returns>
/// The ID of the tenant.
/// </returns>
public Guid SaveRecord(TenantData tenant)
{
	TenantData[] listToSave;
	if(tenant.Id.Equals(Guid.Empty))
	{
		// Record is new
		tenant.Id = Guid.NewGuid();
		tenant.CreatedTime = DateTime.Now;
		listToSave =
			GetAllRecords().Concat(
				new TenantData[] { tenant }
			).ToArray();
	}
	else
	{
		// Existing record
		listToSave =
			GetAllRecords().ToArray();
		for (int index = 0; index < listToSave.Count(); index++)
		{
			if (listToSave[index].Id.Equals(tenant.Id))
			{
				listToSave[index] =
					tenant;
				break;
			}
		}
	}
	SaveAllTenantDataToFile(listToSave);
	return tenant.Id;
}

/// <summary>
/// Deletes a tenant specified by its ID.
/// </summary>
/// <param name="tenantId">The tenant ID.</param>
public void DeleteRecord(Guid tenantId)
{
	SaveAllTenantDataToFile(
		GetAllRecords().Where(
			record => !record.Id.Equals(tenantId)
			)
	);
}

/// <summary>
/// Update the special groups associated with the tenant.
/// </summary>
/// <remarks>
/// After a tenant is created, two groups are also created (Tenant Admin, and Member groups).
/// </remarks>
/// <param name="tenantId">ID of the tenant.</param>
/// <param name="adminGroupId">The tenant admin group ID.</param>
/// <param name="memberGroupId">The tenant group ID.</param>
public void UpdateSpecialGroupIds(Guid tenantId, Guid adminGroupId, Guid memberGroupId)
{
	TenantData[] listToSave =
		GetAllRecords().ToArray();
	for (int index = 0; index < listToSave.Count(); index++)
	{
		if (listToSave[index].Id.Equals(tenantId))
		{
			listToSave[index].AdministratorsGroupId = adminGroupId;
			listToSave[index].MembersGroupId = memberGroupId;
			break;
		}
	}
	SaveAllTenantDataToFile(listToSave);
}

/// <summary>
/// Saves all tenant data to file.
/// </summary>
/// <param name="tenantDataList">The tenant data list.</param>
private void SaveAllTenantDataToFile(IEnumerable<TenantData> tenantDataList)
{
	StringBuilder updatedFileContent = new StringBuilder();
	foreach(TenantData tenantData in tenantDataList)
	{
		updatedFileContent.AppendFormat(
			CultureInfo.CurrentCulture,
			"{0}•{1}•{2}•{3}•{4}•{5}•{6}•{7}•{8}{9}",
			tenantData.Id,
			tenantData.Name,
			tenantData.AdministratorsGroupId,
			tenantData.CreatedTime,
			tenantData.MembersGroupId,
			tenantData.AccountNamePattern,
			tenantData.BrandingSettingOverrides,
			tenantData.DataConnectorPropertyOverrides,
			SerializeLicenseSeatAllocation(tenantData.LicenseSeatAllocation),
			Environment.NewLine
			);
	}
	File.WriteAllText(this.TenantInformationFilePath, updatedFileContent.ToString());
}

Note
Each Tenant will have two groups associated with it. "Tenant Members"; and "Tenant Administrators".

With a read-only tenant it is the tenants responsibility to manage the special group ID's, these are GUIDs and should be the same for a specific tenant. It does not matter if the groups actually exist. In this case the UpdateSpecialGroupIds method will never be called.

If the tenant provider is read/write the when SaveRecord on the Tenant is called, the AdministratorsGroupId and MembersGroupId values should be left be left as empty GUIDs. After SaveRecord is called on the provider for a new tenant, Dundas BI will auto-create the special groups, and then call UpdateSpecialGroupIds so that the provider can save those special group IDs with the tenant.

5.4.3. Implementing of the rest of the tenant interface

This example demonstrates how to implement the other methods in the ITenantsProvider Interface.

/// <summary>
/// Retrieves a list of tenants having the specified IDs.
/// </summary>
/// <param name="tenantIds">The IDs of the tenants to retrieve.</param>
/// <returns>
/// The requested tenants.
/// </returns>
public ICollection<TenantData> GetById(ICollection<Guid> tenantIds)
{
	Collection<TenantData> tenantDataCollection =
		new Collection<TenantData>();
	var allRecords =
		GetAllRecords();
	foreach (Guid id in tenantIds)
	{
		TenantData foundItem =
			allRecords.FirstOrDefault(
				accountDataItem => accountDataItem.Id.Equals(id)
			);
		if (foundItem != null)
		{
			tenantDataCollection.Add(
				foundItem
				);
		}
	}
	return tenantDataCollection;
}

/// <summary>
/// Gets all records.
/// </summary>
/// <returns>
/// All the records managed by the provider.
/// </returns>
public IEnumerable<TenantData> GetAllRecords()
{
	List<TenantData> tenantDataList =
		new List<TenantData>();
	foreach(string line in File.ReadAllLines(
		this.TenantInformationFilePath
		)
	)
	{
		string[] splitLine =
			line.Split('•');
		tenantDataList.Add(
			new TenantData()
			{
					Id = new Guid(splitLine[0]),
					Name = splitLine[1],
					AdministratorsGroupId = new Guid(splitLine[2]),
					CreatedTime = DateTime.Parse(splitLine[3]),
					MembersGroupId = new Guid(splitLine[4]),
					AccountNamePattern = splitLine[5],
					BrandingSettingOverrides = splitLine[6],
					DataConnectorPropertyOverrides = splitLine[7],
					LicenseSeatAllocation = DeserializeLicenseSeatAllocation(splitLine[8])
			}
		);
	}
	return tenantDataList;
}

/// <summary>
/// Gets the IDs of any tenants which draw the specified seat kind from the global pool.
/// </summary>
/// <param name="seatKind">The kind of license seat.</param>
/// <param name="seatCountProperty">The seat count property.</param>
/// <returns>
/// IDs of the tenant which draw from global pool.
/// </returns>
public ICollection<Guid> GetIdsOfTenantsWhichDrawFromGlobalPool(LicenseSeatKind2 seatKind, SeatCountProperty seatCountProperty)
{
	// Only Tenant1 draws from global pool in this example.
	return new Collection<Guid>()
		{
			GetAllRecords().FirstOrDefault(tenantItem => tenantItem.Name.Equals("tenant1")).Id
		};
}

/// <summary>
/// Gets the combined license usage for all tenants.
/// </summary>
/// <returns>
/// A dictionary containing the total number of seats allocated to all tenants, grouped by seat kind.
/// </returns>
public IDictionary<LicenseSeatKind2, SeatCount> GetLicenseAllocation()
{
	return new Dictionary<LicenseSeatKind2, SeatCount>()
	{
		{ LicenseSeatKind2.ReservedDeveloper, new SeatCount(10) },
		{ LicenseSeatKind2.ReservedPowerUser, new SeatCount(10) },
		{ LicenseSeatKind2.ReservedStandardUser, new SeatCount(10) }
	};
}

/// <summary>
/// Imports a tenant to the underlying storage mechanism.
/// </summary>
/// <param name="tenant">The tenant to be imported.</param>
/// <returns>
///   <see langword="true" /> if a new tenant was imported; otherwise <see langword="false" />.
/// </returns>
public bool ImportRecord(TenantData tenant)
{
	IEnumerable<TenantData> allTenants =
		GetAllRecords();  
	TenantData tenantData =
		allTenants.FirstOrDefault(
			tenantItem =>
				tenantItem.Id.Equals(tenant.Id)
		);
	if(tenantData != null)
	{
		// Overwrite existin record.
		tenantData = tenant;
		SaveAllTenantDataToFile(allTenants);
		return false;
	}
	else
	{
		// New record.
		SaveAllTenantDataToFile(
			allTenants.Concat(new TenantData[] {  tenant } )
			);
		return true;
	}
}

5.5. Enabling the custom tenants provider

In order to enable the custom tenants provider you need to do the following:

1. Log on to Dundas BI using the administrator account.

2. Click the Admin toolbar icon:

Admin toolbar icon
Admin toolbar icon

3. Click the Setup section, and then the config button:

Setup config button
Setup config button

4. Set the setting scope to Global:

Scope global
Scope global

5. In the Authentication category with the name Custom Tenants Provider enter the following text:

    0cdf181c-b489-400d-8901-c4cb76abc1d4:CustomTenantsProviderSample:Dundas.BI.Sample.CustomTenantsProviderSample.CustomTenantsProvider

Note

The format of this value is "[pkgId]:[assemblyName]:[typeName]", where:

  • [pkgId] is the ID of the extension package containing the provider (e.g. "37676181-23df-4bdf-b618-79d5f2aa5f52");
  • [assemblyName] is the name of the assembly containing the provider (e.g."MyCompany.DundasExtensions");
  • [typeName] is the full type name of the provider (e.g. "MyCompany.DundasExtensions.MyTenantsProvider").

6. Click the checkmark to save the changes:

Checkmark
Checkmark

7. Reset the Dundas BI application pool

6. Debugging

In order to debug the accounts provider, you can use the following:

System.Diagnostics.Debugger.Launch();

This pop up a window that will allow you to attach the debugger.

Debugging popup
Debugging popup

The following example demonstrates how to attach a debugger when the extension is used:

/// <summary>
/// Gets the name of the extension package.
/// </summary>
public override string Name
{
	get 
    {
		System.Diagnostics.Debugger.Launch();
		return "Custom Tenants Provider Sample Package"; 
    }
}

 

7. See Also

Dundas Data Visualization, Inc.
500-250 Ferrand Drive
Toronto, ON, Canada
M3C 3G8

North America: 1.800.463.1492
International: 1.416.467.5100

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