-
Notifications
You must be signed in to change notification settings - Fork 10.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Re-add proper API calls into Blazor WebAssembly template #55307
Comments
Thanks for adding this! I would like to propose this is two separate issues.
I think 1 is a user experience issue, whereas 2 is a developer experience issue - and as such should be dealt with at separate times. I think it would be easy to solve item 1 without any changes to the Blazor framework by updating the default template to something like the attached. This uses the following approach
This means we can inject IWeatherForcecastService into the /Weather page in order to retrieve the data. When server rendering it will use the server implementation and go straight to the simulated Db, and WASM rendering it will be injected with the proxy class instead and call to the API. The benefits of this are
|
I recently wrote a Blazor Interactive WebAssembly basics article. https://www.telerik.com/blogs/building-interactive-blazor-apps-webassembly The end product was quite lengthy because I had to explain and provide examples of all the issues that arise when using the new Web App template. From the beginning, a selling point of Blazor is it's "low learning curve" for .NET developers. However, we now have a host of new features that improve the framework, but also increase complexity. A template to deal with some of the complexity and set best practices is desperately needed. I think @mrpmorris has a good sample, this is a better organized version of what I wrote in my article. |
At the moment, the current template doesn't show the intention or reason behind the web app project/client project. @mrpmorris Has a good suggestion with the contracts project, demonstrating an interface, the double injection using different providers. An awesome improvement (no idea on the feasibility) would be to make it 'feel' like writing traditional WASM or Server code, but the prerender persistence/auto issues are handled for you (opt in/out?). The ideal dev ex would be quick to figure out intention & not have to google issues (find out you should disable prerendering, which is a must have for SEO). I think optimising for the best DevEx of prerender ON & Auto render mode, will fix a lot of issues on the side. For example, an app I'm playing with using the Blazor Web App paradigm has to write a lot of code to call 1 Web API endpoint, then display that data as is. Example page here: https://github.com/Hona/TempusHub2/blob/main/UI/src/TempusHub.WebUI/TempusHub.WebUI.Client/Components/Pages/Leaderboards/Map/MapPage.razor |
With regards to the double-render, the following code works fine. Note the code to simulate fetching data has been moved into FetchDataAsync for brevity. public partial class Weather : ComponentBase
{
private WeatherForecast[]? forecasts;
[Inject]
private PersistentComponentState PersistentComponentState { get; set; } = null!;
protected override async Task OnInitializedAsync()
{
PersistentComponentState.RegisterOnPersisting(SaveStateAsync);
if (!PersistentComponentState.TryTakeFromJson(nameof(forecasts), out forecasts))
{
await FetchDataAsync();
}
}
private Task SaveStateAsync()
{
PersistentComponentState.PersistAsJson(nameof(forecasts), forecasts);
return Task.CompletedTask;
}
private async Task FetchDataAsync()
{
// ...
}
} Here are my thoughts for improvement. The code above is nice and easy mainly because there is only one property to persist. Once there are multiple calling public partial class Weather : ComponentBase
{
public WeatherForecast[]? Forecasts; // Made public
public int SelectedIndex;
public string SomeOtherState = "This is serialized too.";
protected override async Task OnInitializedAsync(bool stateWasDeserialized)
{
if (!stateWasDeserialized)
{
await FetchDataAsync();
}
}
// ...
}
I think this gives the easiest experience for new users and also allows more experienced users to tweak more precisely if needed.
Possible alternative names for this parameter if (stateIsUnitialized)
await FetchDataAsync(); Another benefit of this approach is that it could be retrofitted to .net 8 using a Roslyn Analyzer, the only difference being there would be no |
@mrpmorris I love that design. I want to leave my thoughts that follow on from your ideas. I believe for the success of Blazor there should be a lot more magic/convenience by default, and it be suggested by way of the templates. The basis is using Auto render mode with prerendering on. My experience of using this is its incredibly clunky & beginner unfriendly (e.g. when does something render, what project do I put this in, is this called on the client, or the server, or both (twice?)) The suggested paradigm relies on server first, client side second. I'd suggest the following structure: Contoso
Contoso.Contracts
Contoso.Client
Now, what would this look like?
@page "/weatherforecast"
@inject IWeatherRepository WeatherRepository
<!-- Put a table here -->
@code {
public WeatherForecast[] Forecasts;
protected override async Task OnInitializedAsync(bool stateWasDeserialized)
{
if (!stateWasDeserialized)
{
Forecasts = await WeatherRepository.GetForecastsAsync();
}
}
}
public record WeatherForecast(...); `Contoso.Contracts/Repositories/IWeatherRepository.cs public interface IWeatherRepository
{
Task<WeatherForecast[]> GetForecastsAsync();
} Now the important stuff, showing which is server, which is client implementations.
public class WeatherRepository(AppDbContext DbContext) : IWeatherRepository
{
public Task<WeatherForecast[]> GetForecastsAsync()
{
// Simulating some sort of EF query
return await DbContext.Forecasts
.ToListAsync();
}
}
public class WeatherRepository(HttpClient Client) : IWeatherRepository
{
public Task<WeatherForecast[]> GetForecastsAsync()
{
// Simulating some sort of EF query
return await Client.GetFromJsonAsync<WeatherForecast[]>("/api/weatherforecasts");
}
} Now the wireup code
...
builder.Services.AddSingleton<IWeatherRepository, WeatherRepository>();
...
...
builder.Services.AddSingleton<IWeatherRepository, WeatherRepository>();
... I think in this way there could even be the possibility to automatically detect what is server only, client only, where there is only a server implementation of the interface, it can only be on the server. There was some code omitted obviously, interactivity models I generally ignored, where prerenders/client and server switches are the priority for me. Further Improvements
I'd even consider a separate method on ComponentBase, taking this snipped from the previous message: protected override async Task OnInitializedAsync(bool stateWasDeserialized)
{
if (!stateWasDeserialized)
{
await FetchDataAsync();
}
} I think this leads to two separate concerns being thrown into one method. I feel there is enough to warrant something like the following (ignoring async overloads): // Note that this method would be written the same with classic blazor paradigms
protected override async Task OnInitializedAsync()
{
FetchDataAsync();
}
// Note that this is opt in, if the framework's magic is not correct for specific use cases
protected override void OnDeserialized()
{
// Do something
// Maybe provide parameters mentioning what changed?
} I think if:
Then, Blazor could be open to wider adoption. Feedback from the 10 or so different people I've seen try the Blazor Web App paradigm, have all come to the same conclusion as me, where you should just stick with the classic project structures. Keen to hear everyone's thoughts :) |
UpdateAfter some internal discussion, our inclination is to not make any further changes to the Blazor Web App project template in .NET 9. The primary reasoning for this is:
I'll use this comment to summarize the internal discussion, including the tradeoffs and potential approaches we considered. We'd greatly appreciate feedback on what specific changes to the template you would like to see, so that the changes we do eventually make will be well-informed. Original proposed changesThe original proposal was to make the following changes to variations of the Blazor Web template that use WebAssembly interactivity only:
The following sections describe some tradeoffs we considered when evaluating these changes. Tradeoff 1: UX regressionOne fairly significant tradeoff is that the user gets a blank screen on the weather page during WebAssembly initialization. This is an especially poor UX on slower connections, where the WebAssembly runtime can several seconds to download and initialize. Potential solution: Add a loading indicatorA loading indicator (similar to the one in the WebAssembly standalone template) could provide some feedback to the user while the WebAssembly runtime is downloading and starting. We can do this by creating a WebAssemblyLoadingIndicator.razor @rendermode InteractiveWebAssembly
@if (!Platform.IsInteractive)
{
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
} This component works by:
When using global WebAssembly interactivity, we can simply plop the One way to achieve this is to move WebAssemblyLoadingIndicator.razor @inherits LayoutComponentBase
@layout MainLayout
<LoadingIndicator />
@Body Pages that want to display a loading indicator can do the following: @page "/weather"
@layout WebAssemblyLoadingLayout
@rendermode @(new InteractiveWebAssemblyRenderMode(false)) This mostly results in a better UX, but:
There are other ways to add the loading indicator to the page that don't require an explicit gesture from the page component, but are a bit too "gross" to put in the template. For example, rather than introducing a new @inherits LayoutComponentBase
<div class="page">
...
<main>
+ @if (!Platform.IsInteractive && PageRenderMode is InteractiveWebAssemblyRenderMode { Prerender: false })
+ {
+ <LoadingIndicator />
+ }
<article class="content px-4">
@Body
</article>
</main>
</div>
...
@code {
+ private IComponentRenderMode? PageRenderMode
+ => (Body?.Target as RouteView)?.RouteData.PageType.GetCustomAttribute<RenderModeAttribute>()?.Mode;
} The general internal consensus was that to address the UX regression, we're introducing another tradeoff adding complexity to the template. Potential solution: Enable prerenderingWe also briefly considered whether there was a way to cleanly make the @page "/weather"
@rendermode InteractiveWebAssembly
@inject? HttpClient Client @* Some kind of new optional inject *@
@code {
protected override async Task OnInitializedAsync()
{
if (Client is not null)
{
forecasts = await Client.GetFromJsonAsync<WeatherForecast[]?>("/api/weather");
}
}
} However, we quickly decided against this since it doesn't broadly solve the problem of being able to write components without having to worry about how they run on the server. Tradeoff 2: Not a great demonstration of interactivityThe weather page currently demonstrates stream rendering, which was one of the major features included in the .NET 8 release. We'd be swapping out that demonstration in favor of a different one that's arguably less suitable for the scenario, given the page is completely static after loading weather data. We've received feedback that demonstrating use of There's also the consideration that #51584 might get implemented in .NET 9. In many cases, that feature could serve as a better solution to the "double fetch" problem than disabling prerendering and using an Requested feedbackTo help us decide what changes to make to the template, we'd love to hear ideas from the community about what specific changes you'd like to see. Please bear in mind that one of our goals is to keep these templates as straightforward and approachable as possible. Thanks! |
I'm 100% fine with the decision made to not change this in .NET 9. Template changes are something we don't want to churn on repeatedly, so doing it earlier in a cycle makes sense. If we come back to this in .NET 10, what about the following ways to address the concerns above? Not sure if this approach has been considered but it seems simpler and more reliable to me:
I haven't tried it but would expect this to cause the loading indicator to not show up (even as a flicker) except when loading is actually happening, and would work the same whether it's global or per-page interactivity. |
@SteveSandersonMS, that's also an approach I tried out, although I ended up with a slightly different version of it: /* Hide the loading indicator if we haven't started loading WebAssembly resources */
html:not([style*="--blazor-load-percentage"]) > * .loading-indicator {
display: none;
}
/* Hide the loading indicator if the current page contains any content */
article > .loading-indicator:not(:only-child) {
display: none;
} I found that relying on a CSS variable that gets removed after loading completes wasn't totally ideal because:
My solution was to add a second CSS rule to hide WebAssembly loading indicator if there's any content on the page. Unfortunately, this was still problematic:
I can think of some other ways to overcome those problems, but this is where I stopped to try an approach that programmatically adds and removes the loading indicator. Maybe the CSS approach is worth revisiting again! |
Seeing this postponed to Net 10, over a year and half from now, when the Net 9 roadmap is barely halfway, and the issues discussed here have been reported long before the Net 8 release is disappointing. I am not sure what additional feedback should be given about what we would like to see in templates: abundant feedback was given before the release of Net 8, and it was often ignored or dismissed by claiming the Web App already has equivalent functionality, even when implementing the feedback in question meant leaving the existing Net 7 templates available side-by-side to the new ones until the latter reached a higher level of maturity. At any rate, my two (old) cents. Having a template where the developer can choose what render mode to use and generate an app that can be used as a starting point without major surgery just to achieve basic functionality, as needed now with the Web App, would be useful. For example:
In these options, demonstrate persistence (it would be a start to have one page demoing pre-rendering persistence). It can be improved later for Net 10, if a better way is found. I think this would also reduce the effort to dig out the information from the documentation, which is sometimes spotty. P.S. Templates have changed drastically in Net 8, significantly increasing the amount of work needed to get up to speed, and deeply changing the development pattern (yes, the previous functionality can be recovered, but also that takes time to figure out). They have also changed after the Net 8 release, to allow integration in Aspire. I don't see much concern with changing templates, so I would say pushing a partial fix to the issues discussed in this thread should have higher priority than not altering templates, which does not seem a concern in other circumstances. |
To be honest, from the beginning I've found that "Blazor Unified", or Auto, is impressive technically but adds too much overthinking and complexity. As a developer you need to decide which render mode you want and answering that question requires a deep knowledge on what it takes. By default VS suggests Auto mode, but when you want to add an API project (which is very common and was implemented before .Net 8) you'll realise that some tradeoffs are needed. Even the full (global) WASM template isn't a ready to go as mentioned earlier in this issue. So the default templates are irrelevant in most use cases and you need to find tricks or disable some things. Currently Auto, or trying to run a component in both Server and WASM is not a good choice IMO. There are a too many differences wether you're running on Server or on Client. Before .Net 8 that choice was made once for all at the start of the project and it was OK because those 2 modes are and will remain different no matter how hard you try to unify them. Finally to me Auto should be a third mode for the adventurers / experts that exactly know what they are doing. But default templates should stay stupid simple, full Server or full WASM, and with WASM comes an API call so you are ready to start developing your solution. .Net 7 WASM template was a real world sample. Now we miss it. When we choose (global) WASM by default we don't want components that are able tu run on the Server. And if it's needed it's up to the developer to do the necessary tricks. To summarise, three templates would be great :
|
As of .NET 8, the Blazor Web template with WebAssembly interactivity does not contain any example of calling a server API. The "weather forecast" page simply generates data on the client and thus only demonstrates how to render a table, not how to fetch data.
Actually calling a server API in all generality, accounting for all the main ways of setting up a site, is complicated by several factors, as pointed out by @mrpmorris:
HttpClient
in DI with a base address on both server and client, and have the server call itself via HTTPWhile typical apps don't usually have to solve all these problems simultaneously, we need to be able to demonstrate convenient, flexible patterns that do compose well with all the features and are clean enough to put them in the project template.
Solving this nicely likely depends on implementing some other features like #51584
Note for community: Recommendation for .NET 8 developers
As mentioned above, typical apps don't normally have to solve all these problems. It's always been simple to load data from the server with Blazor WebAssembly and that is still the case in .NET 8.
The easiest path is simply to disable WebAssembly prerendering, because then just about all the complexity vanishes. Whenever you used rendermode
WebAssembly
(e.g., inApp.razor
), replace it withnew WebAssemblyRenderMode(prerender: false)
.At that point your UI is just running in the browser and you can make
HttpClient
calls to the server as normal.The text was updated successfully, but these errors were encountered: