I'm writing a service that charts some data. The data is retrieved from blob storage by a client that only has an async
Get
method to get the data. Once the data is retrieved it is used to populate the chart and this is done using another method Redraw
that is also async
. I would like the chart to update every minute or so so I have the Get
and Redraw
called inside a while loop with a delay.
private async Task Redraw(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var data = await DataProvider.Get();
await Chart.Redraw(data.Labels, new[] {data.DataSet});
await Task.Delay(TimeSpan.FromSeconds(60), cancellationToken);
}
}
Unfortunately, this fails. I get a NullReferenceException
and a AggregateException
from somewhere in the chart library. Modifying the code as follows does work.
private async Task Redraw(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var data = Task.Run(() => DataProvider.Get()).GetAwaiter().GetResult();
await Task.WhenAll(Chart.Redraw(data.Labels, new[] {data.DataSet}));
await Task.Delay(TimeSpan.FromSeconds(60), cancellationToken);
}
}
I'm not sure why the first version using async/await fails. I believe that it's because the Redraw
is trying to run before the data task is finished but I also believed that awaiting the Get
method would mean that Redraw
would not be called until the data task was complete. When I debug those lines the data does appear to be populated before the next line is executed so i'm a bit confused.
Exception thrown: 'System.NullReferenceException' in Blazorise.Charts.dll Exception thrown: 'System.NullReferenceException' in System.Private.CoreLib.dll 'iisexpress.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.1\System.Diagnostics.StackTrace.dll'. 'iisexpress.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.1\System.Reflection.Metadata.dll'. 'iisexpress.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.1\System.Collections.Immutable.dll'. Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer: Warning: Unhandled exception rendering component: Object reference not set to an instance of an object.
System.NullReferenceException: Object reference not set to an instance of an object. at Blazorise.Charts.JS.ToChartDataSet[T](ChartData
1 data) at Blazorise.Charts.JS.InitializeChart[TItem,TOptions](IJSRuntime runtime, DotNetObjectReference
1 dotNetObjectReference, Boolean hasClickEvent, Boolean hasHoverEvent, String canvasId, ChartType type, ChartData1 data, TOptions options, String dataJsonString, String optionsJsonString) at Blazorise.Charts.BaseChart
4.Initialize()
at Blazorise.Charts.BaseChart`4.OnAfterRenderAsync(Boolean firstRender) Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost: Error: Unhandled exception in circuit 'v_kcV7GT8jgmXOLCuNjM_i918pfAqsaPh5KVrAsZNUw'.System.AggregateException: One or more errors occurred. (Object reference not set to an instance of an object.) ---> System.NullReferenceException: Object reference not set to an instance of an object. at Blazorise.Charts.JS.ToChartDataSet[T](ChartData
1 data) at Blazorise.Charts.JS.InitializeChart[TItem,TOptions](IJSRuntime runtime, DotNetObjectReference
1 dotNetObjectReference, Boolean hasClickEvent, Boolean hasHoverEvent, String canvasId, ChartType type, ChartData1 data, TOptions options, String dataJsonString, String optionsJsonString) at Blazorise.Charts.BaseChart
4.Initialize()
at Blazorise.Charts.BaseChart`4.OnAfterRenderAsync(Boolean firstRender) --- End of inner exception stack trace ---
I'm digging deeper and I don't think the null reference is being throw when calling Chart.Redraw
at all. When this line executes the data is not null. Instead it looks like it is related somehow to async/await. The call that precedes Redraw
looks like this..
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_isAlreadyInitialized && firstRender)
{
_isAlreadyInitialized = true;
await Redraw(_cancellationToken);
}
}
This overrides the method of the base class from the library. The base class implementation is as follows..
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await this.Initialize();
else
await this.Update();
}
It is this call to Initialize
which is calling another method..
private static object ToChartDataSet<T>(ChartData<T> data)
{
return (object) new
{
Labels = data.Labels,
Datasets = ((IEnumerable<object>) data.Datasets).ToList<object>()
};
}
..and it's here where the data is null.
For some reason using await/async in the Redraw
method results in the base class calling Initialize
=> ToChartDataSet
before the call to DataProvider.Get
is complete. When using GetAwaiter().GetResult()
then Initialize
is not called until the DataProvider.Get
has completed.