Report generation interceptors
Report generation interceptors allow you to hook into the report generation process to inspect or modify generated reports before they are delivered to users or processed by callbacks. This is particularly useful for adding metadata, applying transformations, implementing audit trails, or cancelling standard processing.
Overview
When a report is generated (either from the client or from server-side code), you can register interceptors that will be invoked after the PDF is generated but before the notification is sent to the user or the callback is executed.
Important
Interceptors are only invoked for PDF generation (when generating a file to download or process). They are not triggered for report previews displayed in the viewer, as previews do not generate a PDF file.
Interceptors receive a ReportGeneratedInterceptorContext containing:
- The raw PDF content
- The report template name
- The filename to use
- The generation identifier
- The request origin (client or server)
You can:
- Read the report content
- Modify the PDF content by calling
UpdateContent(byte[]) - Cancel standard processing by setting
Cancel = true
Warning
If you want to modify the PDF content in a deployed environnement, you'll need to set reportingTempDirectoryAccessMode to ReadWrite in your cluster backend Helm configuration.
Creating an interceptor
To create an interceptor, implement the IReportGeneratedInterceptor interface:
using System.Threading;
using System.Threading.Tasks;
using GroupeIsa.Neos.Application.Reports;
public class MyReportInterceptor : IReportGeneratedInterceptor
{
public Task OnReportGeneratedAsync(
ReportGeneratedInterceptorContext context,
CancellationToken cancellationToken)
{
// Your logic here
return Task.CompletedTask;
}
}
Example: adding metadata to PDF
This example demonstrates how to add generation metadata (template name, cluster, version, and generation identifier) at the bottom of each page in PDF reports requested by clients:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using GroupeIsa.Neos.Application;
using GroupeIsa.Neos.Application.Reports;
using iText.IO.Font.Constants;
using iText.Kernel.Colors;
using iText.Kernel.Font;
using iText.Kernel.Geom;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
public class ReportGenerationMetadataInterceptor : IReportGeneratedInterceptor
{
private const float FontSize = 8f;
private const float MarginLeft = 10f;
private const float MarginBottom = 10f;
private readonly IApplicationInfo _applicationInfo;
public ReportGenerationMetadataInterceptor(IApplicationInfo applicationInfo)
{
_applicationInfo = applicationInfo;
}
public Task OnReportGeneratedAsync(
ReportGeneratedInterceptorContext context,
CancellationToken cancellationToken)
{
// Only process reports requested by the client
if (context.RequestOrigin != ReportRequestOrigin.Client)
{
return Task.CompletedTask;
}
byte[] modifiedContent = AddGenerationMetadataFooter(
context.ReportContent,
context.TemplateName,
context.GenerationIdentifier);
context.UpdateContent(modifiedContent);
return Task.CompletedTask;
}
private byte[] AddGenerationMetadataFooter(
byte[] pdfContent,
string templateName,
Guid generationIdentifier)
{
string footerText = $"Template: {templateName} | Generated by {_applicationInfo.ClusterName} v{_applicationInfo.ClusterVersion} | ID: {generationIdentifier}";
using MemoryStream inputStream = new(pdfContent);
using MemoryStream outputStream = new();
using PdfDocument pdfDocument = new(
new PdfReader(inputStream),
new PdfWriter(outputStream));
PdfFont font = PdfFontFactory.CreateFont(StandardFonts.HELVETICA);
int numberOfPages = pdfDocument.GetNumberOfPages();
for (int i = 1; i <= numberOfPages; i++)
{
PdfPage page = pdfDocument.GetPage(i);
Rectangle pageSize = page.GetPageSize();
PdfCanvas canvas = new(page);
canvas.BeginText()
.SetFontAndSize(font, FontSize)
.SetColor(ColorConstants.GRAY, true)
.MoveText(
pageSize.GetLeft() + MarginLeft,
pageSize.GetBottom() + MarginBottom)
.ShowText(footerText)
.EndText();
}
pdfDocument.Close();
return outputStream.ToArray();
}
}
Note
This example uses the iText7 library to manipulate PDF content. You'll need to add the appropriate NuGet package references to your business assembly project.
Note
This example uses a HELVETICA font. For this to work in a deployed environment, remember to add it to the backend/taskrunner image of your cluster. See this article for basic cluster DockerFile examples. Example in a cluster docker file :
# --- FROM "harbor.hexanet.fr:8443/neos/neos-fmk:..." AS builder...
# --- FROM mcr.microsoft.com/dotnet/aspnet:...
# Copy your font file in the final image layer :
COPY ./my-helvetica-font.ttf /usr/share/fonts/my-helvetica-font.ttf
# --- USER ...
# --- ENTRYPOINT ...
In this example, image tags are intentionally generic. Keep your actual base image tags aligned with your targeted Neos/.NET minor version and your security update process.
Example: audit logging
using System.Threading;
using System.Threading.Tasks;
using GroupeIsa.Neos.Application.Reports;
public class AuditReportInterceptor : IReportGeneratedInterceptor
{
private readonly IAuditService _auditService;
public AuditReportInterceptor(IAuditService auditService)
{
_auditService = auditService;
}
public async Task OnReportGeneratedAsync(
ReportGeneratedInterceptorContext context,
CancellationToken cancellationToken)
{
await _auditService.LogReportGenerationAsync(
context.TemplateName,
context.GenerationIdentifier,
context.RequestOrigin,
context.ReportContent.Count, // PDF size in bytes
cancellationToken);
}
}
Example: conditional processing cancellation
using System.Threading;
using System.Threading.Tasks;
using GroupeIsa.Neos.Application.Reports;
public class ConditionalCancellationInterceptor : IReportGeneratedInterceptor
{
private readonly IReportApprovalService _approvalService;
public ConditionalCancellationInterceptor(IReportApprovalService approvalService)
{
_approvalService = approvalService;
}
public async Task OnReportGeneratedAsync(
ReportGeneratedInterceptorContext context,
CancellationToken cancellationToken)
{
// Check if the report requires approval
bool requiresApproval = await _approvalService.RequiresApprovalAsync(
context.TemplateName,
cancellationToken);
if (requiresApproval)
{
// Store the report for approval
await _approvalService.StoreForApprovalAsync(
context.GenerationIdentifier,
context.ReportContent.ToArray(),
cancellationToken);
// Cancel standard processing (no notification/callback)
context.Cancel = true;
}
}
}
Registering interceptors
Once you've created your interceptor, you need to register it in your business assembly's Startup.cs file using the extension methods provided by ReportInterceptorServiceCollectionExtensions.
Global registration
To register an interceptor that will be invoked for all report templates:
using GroupeIsa.Neos.Application.Reports;
using Microsoft.Extensions.DependencyInjection;
public static class Startup
{
public static void ConfigureServices(IServiceCollection services)
{
// This interceptor will be invoked for all report generations
services.AddReportGeneratedInterceptor<AuditReportInterceptor>();
}
}
Template-specific registration
To register an interceptor for specific report templates only:
using GroupeIsa.Neos.Application.Reports;
using Microsoft.Extensions.DependencyInjection;
public static class Startup
{
public static void ConfigureServices(IServiceCollection services)
{
// This interceptor will only be invoked for the "Invoice" template
services.AddReportGeneratedInterceptor<InvoiceInterceptor>("Invoice");
// This interceptor will be invoked for multiple templates
services.AddReportGeneratedInterceptor<FinancialReportInterceptor>(
"Invoice",
"OrderConfirmation",
"PaymentReceipt");
}
}
Request origin
The ReportRequestOrigin enum indicates how the report generation was initiated:
| Value | Description | Standard processing |
|---|---|---|
Client |
The report was requested by a user from the frontend (PDF generation, not preview) | A notification will be sent to the user with a download link |
Server |
The report was requested by server-side code using IReportGenerator | The callback method will be executed |
Note
Report previews shown in the viewer do not trigger interceptors because they do not generate a PDF file. Only actual PDF generation requests (download or server-side generation) invoke interceptors.
You can use this information to apply different logic depending on the context:
public Task OnReportGeneratedAsync(
ReportGeneratedInterceptorContext context,
CancellationToken cancellationToken)
{
if (context.RequestOrigin == ReportRequestOrigin.Client)
{
// Logic for user-requested reports
}
else
{
// Logic for server-requested reports
}
return Task.CompletedTask;
}
Execution order
When multiple interceptors are registered (global and/or template-specific), they are executed in the following order:
- Global interceptors are executed first, in the order they were registered
- Template-specific interceptors are executed next, in the order they were registered
Important
If any interceptor sets Cancel = true, the standard processing is cancelled, but all registered interceptors will still be executed. The cancellation only affects the final notification/callback, not the interceptor chain.
Best practices
Dependency injection
Interceptors are instantiated by the dependency injection container, so you can inject services into their constructors:
public class MyInterceptor : IReportGeneratedInterceptor
{
private readonly ILogger _logger;
private readonly IMyService _myService;
public MyInterceptor(ILogger logger, IMyService myService)
{
_logger = logger;
_myService = myService;
}
// ...
}
Performance considerations
- Keep interceptor logic fast to avoid delaying report delivery
- For heavy processing, consider using background tasks
- Be careful when modifying large PDF files
Error handling
If an interceptor throws an exception:
- The report generation is considered failed
- Subsequent interceptors are not executed
- The error is logged and returned to the caller
Therefore, ensure proper error handling within your interceptors if you want to allow processing to continue despite errors:
public async Task OnReportGeneratedAsync(
ReportGeneratedInterceptorContext context,
CancellationToken cancellationToken)
{
try
{
// Your logic
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in report interceptor");
// Don't rethrow if you want other interceptors to continue
}
}
Content modification
When modifying PDF content:
- Always validate the new content before calling
UpdateContent() - Ensure the modified content is still a valid PDF
- Test thoroughly with different report templates
Technical demonstration
A complete working example is available in the TechnicalDemos cluster:
- Interceptor:
demos/technicaldemos/modules/Reports/businessAssembly/Application/Interceptors/ReportGenerationMetadataInterceptor.cs - Registration:
demos/technicaldemos/modules/Reports/businessAssembly/Application/Startup.cs
This example demonstrates how to add generation metadata to the bottom of each page in client-requested PDF reports.
See also
- IReportGeneratedInterceptor - Interceptor interface
- ReportGeneratedInterceptorContext - Context provided to interceptors
- ReportInterceptorServiceCollectionExtensions - Registration extension methods
- ReportRequestOrigin - Request origin enumeration
- Generate reports from business assemblies - Backend report generation using IReportGenerator
- Alternative: modifying PDF in callback - Compare with callback-based modification
Alternative approach: modifying PDF in callback
While interceptors provide a centralized way to modify generated reports, there is an alternative technique: modifying the PDF directly within your callback method and creating a forged response.
How it works
When using IReportGenerator, your callback method receives a ReportGenerationResponse. You can:
- Extract the PDF content from
response.ReportContent - Modify the PDF using libraries like iText7
- Create a new response using
ReportGenerationResponse.ForgeSuccessfulGenerationResponse() - Use the forged response for subsequent operations (notifications, storage)
Example: appending terms and conditions
This example shows how to append a company's terms and conditions PDF to a generated report:
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using GroupeIsa.Neos.Shared.Reports;
using iText.Kernel.Pdf;
using iText.Kernel.Utils;
public class EmailSender
{
private readonly ICompanyRepository _companyRepository;
private readonly ITemporaryFileStorage _temporaryFileStorage;
private readonly IUserNotification _userNotification;
public EmailSender(
ICompanyRepository companyRepository,
ITemporaryFileStorage temporaryFileStorage,
IUserNotification userNotification)
{
_companyRepository = companyRepository;
_temporaryFileStorage = temporaryFileStorage;
_userNotification = userNotification;
}
// This method is called by IReportGenerator as the callback
public async Task SendEmailWithReportAttachmentAsync(
ReportGenerationResponse response,
string companyName)
{
if (response.GenerationSucceed)
{
// Get company terms and conditions
Company? company = await _companyRepository.GetQuery()
.Where(c => c.Name == companyName)
.SingleAsync(CancellationToken.None);
BinaryFile? termsAndConditionsFile = company?.TermsAndConditionsFile;
if (termsAndConditionsFile != null && response.ReportContent != null)
{
// Merge the generated report with the terms and conditions
byte[] mergedContent = MergePdfDocuments(
response.ReportContent,
termsAndConditionsFile.Content);
// Create a forged response with the merged PDF
ReportGenerationResponse forgedResponse =
ReportGenerationResponse.ForgeSuccessfulGenerationResponse(
response.FilenameToUse,
documentContent: mergedContent);
// Replace the original response with the forged one
response = forgedResponse;
}
// Send notification with the (potentially modified) report
ReportGenerationSucceededNotificationArgs notifArgs =
await ReportGenerationSucceededNotificationArgs
.CreateFromResponseAsync(
_temporaryFileStorage,
response,
logger: null,
CancellationToken.None);
await _userNotification.SendReportGenerationSucceededNotificationAsync(
notifArgs,
CancellationToken.None);
}
}
private static byte[] MergePdfDocuments(byte[] pdfContent1, byte[] pdfContent2)
{
MemoryStream outputStream = new();
using (PdfDocument outputDocument = new(new PdfWriter(outputStream)))
{
outputDocument.GetWriter().SetCloseStream(false);
PdfMerger merger = new(outputDocument);
using (MemoryStream stream1 = new(pdfContent1))
using (PdfDocument document1 = new(new PdfReader(stream1)))
{
merger.Merge(document1, 1, document1.GetNumberOfPages());
}
using (MemoryStream stream2 = new(pdfContent2))
using (PdfDocument document2 = new(new PdfReader(stream2)))
{
merger.Merge(document2, 1, document2.GetNumberOfPages());
}
merger.Close();
}
outputStream.Seek(0, SeekOrigin.Begin);
return outputStream.ToArray();
}
}
ForgeSuccessfulGenerationResponse method
The ReportGenerationResponse.ForgeSuccessfulGenerationResponse() factory method creates a new response object that resembles what the reporting service would have produced. It uses the framework's legacy mode capabilities to automatically store the document in the $NeosFile database table.
// Create a forged response from raw bytes
ReportGenerationResponse forgedResponse =
ReportGenerationResponse.ForgeSuccessfulGenerationResponse(
filenameToUse: "modified-report.pdf",
documentContent: modifiedPdfBytes);
// Or from Base64 (if already encoded)
ReportGenerationResponse forgedResponse =
ReportGenerationResponse.ForgeSuccessfulGenerationResponseFromBase64Content(
filenameToUse: "modified-report.pdf",
documentContentInBase64: base64EncodedPdf);
Important
The forged response uses the legacy storage mode, which stores the PDF in the database. This ensures compatibility with the notification and download APIs.
Comparison: interceptors vs callback modification
| Aspect | Interceptors | Callback modification |
|---|---|---|
| When to use | Automatic, cross-cutting modifications | Contextual, business-specific modifications |
| Configuration | Centralized in Startup.cs |
Implemented in each callback |
| Scope | All reports (global) or specific templates | Only for reports using IReportGenerator |
| Code location | Separate interceptor classes | Within callback methods |
| Reusability | High (register once, applies everywhere) | Low (per-callback implementation) |
| Flexibility | Limited to report context | Full access to callback context and dependencies |
| Testing | Test interceptor independently | Test with full callback logic |
| Client requests | ✅ Works (for PDF generation) | ❌ Not applicable (no callback) |
| Server requests | ✅ Works | ✅ Works |
| Complexity | Low (framework handles invocation) | Medium (manual response manipulation) |
| Performance | Overhead for all matching reports | Overhead only when modification is needed |
When to use each approach
Use interceptors when:
- You need to apply the same modification to multiple reports
- The modification logic is independent of the callback context
- You want centralized, declarative configuration
- You need to modify reports requested from both client and server
Use callback modification when:
- The modification depends on business logic or callback parameters
- You need different modifications for different scenarios
- You only generate reports from server-side code (
IReportGenerator) - You want explicit control over when and how the PDF is modified
Use both when:
- You have common modifications (interceptors) + specific ones (callback)
- For example: watermark for all reports (interceptor) + append terms for invoices (callback)
Tip
Both techniques can be combined. Interceptors run first, then your callback receives the already-modified PDF and can apply additional transformations.
Technical demonstration
A complete working example of callback-based PDF modification is available in the TechnicalDemos cluster:
- Callback with modification:
demos/technicaldemos/modules/Reports/businessAssembly/Application/EmailSender.cs(seeActualNotificationSendingAsyncmethod) - UI view:
demos/technicaldemos/modules/Reports/metadata/UIViews/ReportsExecutePdfFromAssemblyUI.yml
This example demonstrates how to append a company's terms and conditions to a generated report.