Report retrieval migration from legacy mode to NAS-based storage
This guide explains how to migrate from the legacy Pub/Sub Base64 storage mode to the new NAS-based UTF8 storage mode when retrieving reports programmatically.
Overview
The report generation system in Neos has evolved to support two storage modes:
- Legacy mode: Reports are stored in the local database as Base64-encoded content using Pub/Sub messaging
- NAS-based mode: Reports are stored as UTF8 files on Network Attached Storage (NAS)
When retrieving reports generated for a business assembly, the handling is optimized to avoid unnecessary database storage when using NAS-based mode.
Additionally, when using the NAS-based storage mode, the resulting PDF file can be handled directly as the actual UTF-8 file instead of being transmitted via a Base64 string which requires extra CPU consumption for unnecessary encoding.
Migration from versions prior to Neos v2.5
To illustrate the migration, let's take the example of a successful report generation response that is forwarded as a notification to the end user. Here we assume that the property ReportGenerationResponse.GenerationSucceed is true, and we will focus on handling the actual content.
Old approach - Not recommended
A typical legacy implementation would always store reports in the local database regardless of the storage mode configuration. For instance:
/// <summary>
/// Store in the local database (REGARDLESS OF STORAGE MODE) and send a download notification.
/// </summary>
/// <remarks>
/// The file will be stored in the local database even when not in legacy mode, which is not optimal.
/// </remarks>
private async Task SendDownloadNotificationAsyncV1(ReportGenerationResponse response, INeosLogger? logger = null, CancellationToken cancellationToken = default)
{
_ = logger; // No logging in this example
// Null-forgiving operator: we assume this method was called when generation succeeded
// (response.GenerationSucceed was true), hence the content cannot be null by contract.
string base64Content = response.ReportContentInBase64!;
byte[] reportContent = Convert.FromBase64String(base64Content);
BinaryFile binaryFile = new($"{response.FilenameToUse}.pdf", reportContent, BinaryFile.PdfMimeType);
Guid downloadIdentifier = await _temporaryFileStorage.AddAsync(binaryFile, cancellationToken);
ReportGenerationSucceededNotificationArgs notificationArgs = new(response.GenerationIdentifier, downloadIdentifier);
await _userNotification.SendReportGenerationSucceededNotificationAsync(notificationArgs, cancellationToken);
}
Issues with this approach:
- Reports were always stored in the database, even when using NAS-based storage
- Redundant storage leading to increased database size
- No optimization based on the configured storage mode (unnecessary conversions back-and-forth between UTF8 and Base64)
New APIs (Neos v2.5)
Two new extension methods have been introduced to simplify report retrieval. Both methods automatically detect whether the system is using legacy or NAS-based storage and only store in the database when necessary.
Both APIs use the already existing TemporaryFileStorage which must already be present in your code, making it easy to replace without having to modify services injected in your existing classes.
Low-level ReportContent property
In the ReportGenerationResponse, next to the old ReportContentInBase64 you will find a new ReportContent property.
This property contains the actual UTF-8 content of the generated file.
- In legacy mode it is automatically decoded from Base64 for you
- and it does not require decoding at all in NAS-based storage mode.
To avoid unnecessary conversions between UTF-8 and Base64, it is recommended to use the property that suits best your need. In most cases, the new ReportContent property containing the final UTF-8 content is what you need.
Mid-level GetDownloadIdentifierAsync file storage
This mid-level extension method available on the ReportGenerationResponse received by your callback provides a simplified way to get a download identifier for the generated report:
Guid downloadIdentifier = await response.GetDownloadIdentifierAsync(_temporaryFileStorage, logger, cancellationToken);
The returned downloadIdentifier behaves differently depending on the storage mode: in NAS-based mode, it corresponds to the original generationIdentifier that was also used to persist the file on disk, while in legacy mode, it represents a fileIdentifier in the database.
High-level CreateFromResponseAsync notification creation
This higher-level factory method available on ReportGenerationSucceededNotificationArgs is the recommended approach when you only need to send a standard notification (i.e. the one with the Download and Print buttons) to the end user:
ReportGenerationSucceededNotificationArgs notificationArgs = await ReportGenerationSucceededNotificationArgs
.CreateFromResponseAsync(_temporaryFileStorage, response, logger, cancellationToken);
New approach - Recommended
Using the new approach, we can rewrite the previous example as follows:
/// <summary>
/// Store in the local database if using legacy mode and send a download notification.
/// </summary>
private async Task SendDownloadNotificationAsync(ReportGenerationResponse response, INeosLogger? logger = null, CancellationToken cancellationToken = default)
{
ReportGenerationSucceededNotificationArgs notificationArgs = await ReportGenerationSucceededNotificationArgs
.CreateFromResponseAsync(_temporaryFileStorage, response, logger, cancellationToken);
await _userNotification.SendReportGenerationSucceededNotificationAsync(notificationArgs, cancellationToken);
}
Benefits of the new approach:
- Reports are stored in the database only when using legacy mode
- When using NAS-based storage, reports are referenced directly without database duplication
- Optimized storage usage and improved performance
- The new APIs (
ReportGenerationResponse.GetDownloadIdentifierAsyncandReportGenerationSucceededNotificationArgs.CreateFromResponseAsync) handle the storage mode detection automatically
Migration steps
To migrate your existing code:
- Remove manual binary file creation and temporary storage calls.
- Replace direct instantiation of
ReportGenerationSucceededNotificationArgswith the factory method - and/or use
GetDownloadIdentifierAsyncis you need the download identifier
For instance:
Before Neos v2.5:
string base64Content = response.ReportContentInBase64!; // On success only
byte[] reportContent = Convert.FromBase64String(base64Content);
BinaryFile binaryFile = new($"{response.FilenameToUse}.pdf", reportContent, BinaryFile.PdfMimeType);
Guid downloadIdentifier = await _temporaryFileStorage.AddAsync(binaryFile, cancellationToken);
ReportGenerationSucceededNotificationArgs notificationArgs = new(response.GenerationIdentifier, downloadIdentifier);
Starting from Neos v2.5:
In two instructions, with explicit acquisition of a download identifier:
Guid downloadIdentifier = await response.GetDownloadIdentifierAsync(_temporaryFileStorage, logger, cancellationToken);
ReportGenerationSucceededNotificationArgs notificationArgs = new(response.GenerationIdentifier, downloadIdentifier);
or simply:
ReportGenerationSucceededNotificationArgs notificationArgs = await ReportGenerationSucceededNotificationArgs
.CreateFromResponseAsync(_temporaryFileStorage, response, logger, cancellationToken);
Best practices
- Always use the new APIs for new implementations
- Migrate existing legacy code to benefit from storage optimization
- Let the framework handle storage mode detection instead of implementing custom logic
- Test thoroughly when migrating to ensure proper functioning in both storage modes
- Prefer using
ReportContentproperty over the legacyReportContentInBase64for improved performance when all you need is the UTF-8 content.
See also
- Interface
IReportGeneratorusage - (Backlink) Report generation triggered from Business assembly and usage IReportGeneratorAPI documentation - Core report generation API