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
- Azure subscription (create one for free or try Azure Cosmos DB for free without an Azure subscription)
- .NET Core 2.1 SDK or later
- Microsoft.Azure.Cosmos NuGet Package.
- DevExpress Reporting must be installed on your machine (download free 30-day trial)
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:
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>(); ... }
Note: the 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.