Communicating with the Reports API via Blazor WASM

2 Answers 181 Views
.NET Core Report Viewer - Blazor
Christiaan
Top achievements
Rank 1
Iron
Christiaan asked on 12 Feb 2024, 02:49 PM

Alright, this might be a long post, but I am quite confused with something. So, I have a Blazor WASM app. In here I get a list of reports using the following code:

Reports = await Http.GetFromJsonAsync<List<string>>($"{BaseAddress}/getreports") ?? new List<string>();


When I select one (because that code is in my NavMenu, I build up a list of the reports there), I then do an OnInitializedAsync and set the ReportSourceOption:
Report Viewer Code:

<div style="padding-top:10px;">
    <ReportViewer @ref ="_reportViewer1"
                       ViewerId="rv1"
                       ServiceUrl="http://localhost:59655/api/reports"
                       ReportSource="@(new ReportSourceOptions()
                              {
                                    Report = ReportName
                              })"
                       Parameters="@(new ParametersOptions { Editors = new EditorsOptions { MultiSelect = EditorType.ListView, SingleSelect = EditorType.ComboBox }})"
                       ScaleMode="@(ScaleMode.Specific)"
                       Scale="1.0"
                       EnableAccessibility="false" />
</div>


OnInitializedAsync Code:

protected override async Task OnInitializedAsync()
{
    await _reportViewer1.SetReportSourceAsync(new ReportSourceOptions
        {
            Report = ReportName
        });
}


Now, in my Blazor App I set default headers. This is important, because these headers will determine which connection string to use, as we have a Multi-Tenant system:

"DefaultHeaders": [
  {
    "Name": "TenantName",
    "Value": "TenantId"
  }
]


When I render the Report Viewer page, it hits the Reports REST service (as expected), but here is the first question I have. Why does my CustomReportSourceResolver get hit up to 6 times? Reason I am asking, is because when my CustomReportSourceResolver constructor gets hit the first time, the header with the TenantId is there. But, the second time the constructor gets hit, it is as if Telerik creates a new Http Context, which in turn clears the headers and then I do not have access to the TenantId.

Here is my CustomReportSourceResolver constructor:

private static string _tenantId;
public CustomReportSourceResolver(IConfiguration configuration, IHttpContextAccessor context)
{
    _configuration = configuration;
    _contextAccessor = context;

    if (!string.IsNullOrEmpty(_contextAccessor.HttpContext.Request.Headers["TenantId"]))
    {
        _tenantId = _contextAccessor.HttpContext.Request.Headers["TenantId"];
    }
}

I had to add the if (!string.IsNullOrEmpty) and set the static string, because the headers kept clearing, but this could cause problems if there are too many users using the report service at the same time. Well, if I am thinking quickly, because I feel like the Custom Resolver will get confused with the _tenantId. If I am wrong, please do let me know.

And then my Resolve method:

public ReportSource Resolve(string report, OperationOrigin operationOrigin, IDictionary<string, object> currentParameterValues)
{
    var tenantId = _contextAccessor.HttpContext.Request.Headers["TenantId"].ToString();

    var json = System.IO.File.ReadAllText($"{configurationsDirectory}\\database.json");

    Settings databaseSettings = JsonSerializer.Deserialize<Settings>(json);

    foreach (var tenant in databaseSettings.DatabaseSettings.TenantSettings)
    {
        if (tenant.TenantId == _tenantId)
        {
            _connectionString = tenant.ConnectionString;
        }
    }

    using (var conn = new NpgsqlConnection(_connectionString))
    {
        conn.Open();

        var sourceReportSource = new UriReportSource { Uri = $"{report}.trdp" };
        var reportSource = UpdateReportSource(sourceReportSource);

        return reportSource;
    }
}

So, as you can see, I have a var tenantId at the top, but that will ALWAYS be null, due to the headers being cleared.

Here is my Program.cs where I inject the CustomReportSourceResolver, as well as my HttpContextAccessor:

builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<IReportSourceResolver, CustomReportSourceResolver>();
builder.Services.TryAddScoped<IReportServiceConfiguration>(sp =>
    new ReportServiceConfiguration
    {
        ReportingEngineConfiguration = sp.GetService<IConfiguration>(),
        HostAppId = "MyHostAppId",
        Storage = new FileStorage(),
        ReportSourceResolver = sp.GetService<IReportSourceResolver>()
    });

If I cannot work around the header being cleared, because I am not sure what Telerik does in the background, so I cannot confirm if they create a new Http Context each time the constructor gets hit, is there a different way I can pass through the TenantId to my CustomReportSourceResolver? I have tried using the report parameters, but that is obviously (at least I think so) used for parameters that is present in the .trdp file, so I am unable to use that. Any help would be appreciated.

I would also like to add that I am using PostgreSQL, not sure if that info is needed. And then this project is being built in .NET 7,  but I am planning on upgrading to .NET 8.

I would also like to apologise if the question I posted is stupid, as I am new to Blazor and to Telerik.

2 Answers, 1 is accepted

Sort by
0
Accepted
Christiaan
Top achievements
Rank 1
Iron
answered on 20 Feb 2024, 01:19 PM
So, I managed to find a solution. It might not be a very great solution, but because I was pressed for time, what I did was I added the header that I want to pass through as part of the report name, and then in my Report Source Resolver, I split the Report Name (It would look like this ID=tenantId;ReportName=reportName), and then I would split at the semicolon, and then store it into a List and then yeah.

After that I would create a new HttpClient instance, add a new header and then pass that value through. Seems to work.
1
Dimitar
Telerik team
answered on 15 Feb 2024, 12:28 PM

Hello Christiaan,

Thank you for the provided information!

The IReportSourceResolver's Resolve method is called multiple times depending on what operation the report viewer is performing. For example, if it tries to render a new report on the viewer, it will call the Resolve method three times each with a different OperationOrigin value.

For the resolver to be hit 6 times, it would need to try and render 2 reports or the same one twice. I noticed that you are passing the ReportName blazor component parameter to the ReportSourceOptions.Report property of the viewer. In this case, you do not need to call the SetReportSourceAsync method as the report viewer will already be trying to render with the value passed to the component. You should need to invoke the SetReportSourceAsync only if the report viewer has to load another report later on.

With that being said, on the issue with the HTTP context, it could indeed be related to the resolver being hit multiple times. I recommend trying to access the context through the UserIdentity class, for example:

            public ReportSource Resolve(string reportId, OperationOrigin operationOrigin, IDictionary<string, object> currentParameterValues)
            {
                var context = Telerik.Reporting.Processing.UserIdentity.Current.Context; 
                ...
            }

If the tenant ID is lost even with this approach, you may override the GetUserIdentity() method of the ReportsController and manually add the tenant to the context, for example:

        protected override UserIdentity GetUserIdentity()
        {
            var userIdentity = base.GetUserIdentity();
            userIdentity.Context ??= new System.Collections.Concurrent.ConcurrentDictionary<string, object>();
            userIdentity.Context.Add("TenantId", "1");
            
            return userIdentity;
        }

Please try this approach and let me know if that works for your needs.

Regards,
Dimitar
Progress Telerik

Stay tuned by visiting our roadmap and feedback portal pages, enjoy a smooth take-off with our Getting Started resources, or visit the free self-paced technical training at https://learn.telerik.com/.
Christiaan
Top achievements
Rank 1
Iron
commented on 15 Feb 2024, 04:26 PM | edited

So, it's a bit of a good new bad news situation. Turns out I got something confused. Even though we have a multi-tenant system, we also have a multi-connect system (one tenant, multiple DBs). So, I should be working with multi-connect. Now, I have a problem.

I still have my reports API, as well as my Blazor WASM application. Now, the strange thing is, when I make a call to my reports API (I have a call GetReports), I am able to pass through a header with the Key-Value: Site, 01. No problem hitting that Get request, or any of the other Get requests. The problem comes with the report viewer. So, my report viewer is pointing towards the correct service URL, but when it tries to make the call /resources/js/telerikReportViewer, it does NOT pass the header through.

I have the following code:

protected override async Task OnInitializedAsync()
{
    using (var client = new HttpClient())
    {
        client.DefaultRequestHeaders.Add("Site", "01");

        ReportService = configuration.GetSection("ReportServiceUrl").Get<string>();

        BaseAddress = $"{ReportService}/api/reports";

        //This call works
        Reports = await client.GetFromJsonAsync<List<string>>($"{BaseAddress}/getreports") ?? new List<string>();

        //This call works
        ClientSites = await client.GetFromJsonAsync<Dictionary<string, string>>($"{BaseAddress}/getmulticonnectvalues") ?? new Dictionary<string, string>();

        //As soon as this gets hit and it tries to make the /resources/js/telerikReportViewer call, it does NOT work. Headers aren't passed through for some reason.
        await reportViewer1.SetReportSourceAsync(new ReportSourceOptions
            {
                Report = "Path//To//Report"
            });
    }
}

So, my question now is, how can I ensure that in my Blazor WASM app that the header Site with its value gets passed through?

Edit: I mean how can I ensure that Telerik actually does take the header and pass it to the API? Because what happens is, when I try to render the Report Viewer, it does not render. I inspect and I see that it was unable to successfully execute the request /resources/js/telerikReportViewer (Internal Server Error 500), due to the header "Site" not being passed through, so it's my API telling me invalid multi-connect header value. 
Dimitar
Telerik team
commented on 20 Feb 2024, 12:35 PM

Hello Christiaan,

Is it not possible to exclude certain endpoints from needing that header? I ask this because the requests for the resources of the report viewer are not authorized even with the viewer's default option as they do not contain any sensitive data and adding your custom header may not be so easy.

For the JS resource especially, it must be added as a script reference and there isn't really a way to add a custom header through the element - javascript - is it possible to set custom headers on js <script> requests? - Stack Overflow

If that is a problem, I would recommend referencing the script from a local folder of your project. You may find all viewer resources in the install directory - C:\Program Files (x86)\Progress\Telerik Reporting <Release>\Html5\ReportViewer.

Once loaded, the viewer's JS will automatically try to load the template which will in turn request the CSS resources. You may also reference those from a local path as the report viewer exposes a TemplateUrl property.

Tags
.NET Core Report Viewer - Blazor
Asked by
Christiaan
Top achievements
Rank 1
Iron
Answers by
Christiaan
Top achievements
Rank 1
Iron
Dimitar
Telerik team
Share this question
or