ASP.NET Core Reporting - Store Reports within a Database Using Azure Cosmos DB

Reporting Team Blog
05 August 2019

In this post, we’ll describe how to store your ASP.NET Core reports using Azure Cosmos DB and describe how to manage user roles.

Prerequisites – What You’ll Need

Implementation Details

To get started, create a sample project with our ASP.NET Core Reporting Project Template and then declare a new code unit - CosmosReportStorageWebExtension.cs. To implement a custom report storage, we need to inherit our class from the DevExpress.XtraReports.Web.Extensions.ReportStorageWebExtension and override all its public methods. You can find a description of these methods in our Custom Report Storage Implementation help topic.

Note: in the non-Windows environment, you can use the approach demonstrated in the ASP.NET Core Reporting - CLI Templates post to create a sample project.

Azure Cosmos DB Storage Access

Accessing an Azure Cosmos DB database requires the following information:

  • DatabaseID
  • ContainerID
  • Connection

The Azure Cosmos DB offers a wide range of API interfaces. In this post we’ll store reports in MongoDB using specifically designed API’s.

1) Follow Microsoft’s Create an Azure Cosmos account tutorial and create an instance of the following API: Azure Cosmos DB for MongoDB API.

2) Select the Quick Start tab to obtain your connection string:

MongoDB - Quick Start Tab

3) Return to the application and open the appSettings.json file and declare the CosmosSettings section as follows:

"CosmosSettings": {
    "DatabaseId": "TestDb",
    "ContainerId": "Reports",
    "SqlConnection": "<sql connection>",
    "MongoConnection": "<mongo connection>"
  }

We’ll use these details to instantiate the MongoClient class at runtime and connect to the database. In this example we’ll create a unified reusable interface should you wish to use a different API to access Azure Cosmos DB (instead of MongoDB).

4) Declare the following interface (implements basic read and update operations required by the report storage):

public interface ICosmosApiStorage { 
    Task UpdateReportItem(ReportItem reportItem); 
    Task<string> CreateReportItem(ReportItem reportItem); 
    Task<ReportItem> GetReportItem(string id); 
    Task<Dictionary<string, string>> GetReports(); 
}

Where the ReportItem class stores the report ID and its layout:

public class ReportItem {
    public byte[] ReportLayout { get; set; }
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }
}

5) Declare the following class (designed specifically for the MongoDB API):

public class ReportItemMongo {
    public static ReportItemMongo CreateFromReportItem(ReportItem item) {
        return new ReportItemMongo() {
            ReportLayout = item.ReportLayout,
            ReportName = item.Id
        };
    }
    [JsonProperty(PropertyName = "_id")]
    public BsonObjectId id { get; set; }
    [JsonProperty(PropertyName = "ReportLayout")]
    public byte[] ReportLayout { get; set; }
    [JsonProperty(PropertyName = "reportName")]
    public string ReportName { get; set; }
    }

This class differs from the ReportItem class by the id property. This BsonObjectId property type is required for incremental indexing of MongoDB database records.

6) Use the MongoClient class methods to implement database access:

public class CosmosMongoApiStorage : ICosmosApiStorage {
    readonly MongoClient mongoClient;
    string databaseId;
    string containerId;
    IMongoCollection<ReportItemMongo> mongoCollection {
        get {
            return mongoClient.GetDatabase(databaseId).GetCollection<ReportItemMongo>(containerId);
        }
    }

    public static void Register(IServiceCollection serviceProvider, IConfiguration cosmosConfig) {
        var storage = new CosmosMongoApiStorage(cosmosConfig["MongoConnection"], cosmosConfig["DatabaseId"], cosmosConfig["ContainerId"]);
        serviceProvider.AddSingleton<ICosmosApiStorage>(storage);

    }

    public CosmosMongoApiStorage(string connection, string databaseId, string containerId) {
        this.databaseId = databaseId;
        this.containerId = containerId;
        MongoClientSettings settings = MongoClientSettings.FromUrl(new MongoUrl(connection));
        settings.SslSettings = new SslSettings() { EnabledSslProtocols = SslProtocols.Tls12 };
        mongoClient = new MongoClient(settings);
    }

    public async Task<ReportItem> GetReportItem(string id) {
        var item = (await mongoCollection.FindAsync(x => x.ReportName == id)).FirstOrDefault();
        return new ReportItem() {
            Id = item.ReportName,
            ReportLayout = item.ReportLayout
        };
    }

    public async Task UpdateReportItem(ReportItem reportItem) {
        UpdateDefinition<ReportItemMongo> updateDefinition = Builders<ReportItemMongo>.Update.Set(x => x.ReportLayout, reportItem.ReportLayout);
        await mongoCollection.UpdateOneAsync(x => x.ReportName == reportItem.Id, updateDefinition);
    }

    public async Task<string> CreateReportItem(ReportItem reportItem) {
        await mongoCollection.InsertOneAsync(ReportItemMongo.CreateFromReportItem(reportItem));
        return reportItem.Id;
    }

    public async Task<Dictionary<string, string>> GetReports() {
        return mongoCollection.AsQueryable().ToDictionary(x => x.ReportName, x => x.ReportName);
    }
}

Report Storage and Database Access

We need to connect our database access client implemented above with the report storage. Documentation recommends storing MongoClient instances in a global loication, either as a static variable or in an IoC container with a singleton lifetime.

1) Maintain our own data access class as a single instance in the Register method:

public static void Register(IServiceCollection serviceProvider, IConfiguration cosmosConfig) {
  ...
    serviceProvider.AddSingleton<ICosmosApiStorage>(storage);
  ...    
}

The CosmosMongoApiStorage.Register static method adds its own class instance to the global IServiceCollection collection of services. This allows you to access the CosmosMongoApiStorage class methods when implementing the report storage.

2) Modify the ReportStorageWebExtension class constructor and inject an instance of the ICosmosApiStorage interface:

readonly ICosmosApiStorage cosmosApiStorage; 

public CosmosReportStorageWebExtension(ICosmosApiStorage cosmosApiStorage) { 
    this.cosmosApiStorage = cosmosApiStorage; 
}

Report Storage and User Identification

We now need to take care of user identity in the report storage and filter reports that are available for two user roles: a report designer and report viewer.

In ASP.NET Core applications, the IHttpContextAccessor.HttpContext.User property value provides access to user identity. We’ll access this interface in our report storage.

1) Open the Startup.cs code unit and add the AddHttpContextAccessor method call to the ConfigureServices method:

public void ConfigureServices(IServiceCollection services) {
...
  services.AddHttpContextAccessor();
...
}

This action will add a default implementation for the IHttpContextAccessor service to the collection of services.

2) Inject this service into the constructor of the report storage:

readonly ICosmosApiStorage cosmosApiStorage;
readonly IHttpContextAccessor httpContextAccessor; 

public CosmosReportStorageWebExtension(ICosmosApiStorage cosmosApiStorage, IHttpContextAccessor httpContextAccessor) { 
  this.cosmosApiStorage = cosmosApiStorage; 
  this.httpContextAccessor = httpContextAccessor;
}

3) We need to register our custom implementation of the ReportStorageWebExtension class. We have two options:

Register it as a singleton:

public void ConfigureServices(IServiceCollection services) {
...
    services.AddSingleton<ReportStorageWebExtension, CosmosReportStorageWebExtension>();
...
} 

Register it as a scoped service. This is useful if you have implemented your own scoped services and need to access its data in the report storage:

public void ConfigureServices(IServiceCollection services) {
...
    services.AddScoped<ReportStorageWebExtension, CosmosReportStorageWebExtension>();
...
}

Notethe latter option is available in the next minor update of our DevExpress Reporting (v19.1.5).

4) The last step is to register our data access client and use Azure Cosmos DB settings stored in the configuration section:

public void ConfigureServices(IServiceCollection services) { 
... 
  CosmosMongoApiStorage.Register(services, Configuration.GetSection("CosmosSettings"));
...
}

Report Storage Methods - Implementation

The following code will restrict a user from creating, editing and storing a report within the database.

1) Declare the following method in the CosmosMongoApiStorage class to check user identity using the IHttpContextAccessor.HttpContext.User property:

bool IsAdmin()
{
    return httpContextAccessor.HttpContext.User.IsInRole("Admin"); ;
}

2) Use this method in the CanSetData / GetData method calls as follows:

public override bool CanSetData(string url)
{
   return isAdmin();
}

public override byte[] GetData(string url)
{
    if (!isAdmin()) throw new UnauthorizedAccessException();
    ...
}

A Sample Project Is Available

You can download the complete sample here.

The report storage implementation described herein supports both the SQL and MongoDB API. Use the CosmosClient class located in the Microsoft.Azure.Cosmos NuGet Package, if using the SQL API (for Azure Cosmos DB Storage access).

We used the Use cookie authentication without ASP.NET Core Identity tutorial to implement authentication for this sample project.

Future Plans - Your Feedback Counts

We ship a special DevExpress.Web.Reporting.Azure NuGet package that contains services designed to work with web reporting applications in the Microsoft Azure environment. We created these services to help you integrate our web reporting components in ASP.NET WebForms and MVC apps. The following method calls switch the Web Document Viewer and Web Report Designer components to store documents and their internal data within specifically designed Azure storage.

AzureWebDocumentViewerContainer.UseAzureEnvironment(cloudStorageConnectionString);
AzureReportDesignerContainer.UseAzureEnvironment(cloudStorageConnectionString);

You can learn more about our implementation in the following help topic: Microsoft Azure Reporting.

Note: This API is not yet available for the ASP.NET Core platform. Share your thoughts below and help us better serve your needs in both the short and long term.



no comments
No Comments

Please login or register to post comments.