-
Notifications
You must be signed in to change notification settings - Fork 10.2k
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 :
|
This situation is insane. I'm so glad I started with Blazor WebAssembly in 2020, and got a proper project template (which I now miss dearly) that gave me:-
and showed me the all-important Synthesizing data on the client and pretending you got it from the server, and not showing how you do actually get data from the server... that's nuts. And all the new rendering options and complexity you've added since 2020... I'm not remotely interested in any of it. I can't even find a modern Blazor template that gives me Client, Server, Shared. But surely it's still a viable model? @SteveSandersonMS has pointed out the need to show the @danroth27 pointed it out in 2023: #51204 I pointed it out also in 2023: #52474 ... and yet still you don't do it. I'd hate to start using Blazor WebAssembly now, today. I like it, but I thank the heavens I started in 2020. Now it's a nightmare of complexity and incompleteness in the templates. I do strongly believe that this template problem, and also the issues pointed out by @DjeeBay above relating to complexity, could well have a significant, negative impact on adoption rates of Blazor.
^^ this! If I was someone new to Blazor, how could I answer these types of questions in the template? Again: thank goodness I didn't have to answer them in 2020. And we do want new adopters for Blazor, don't we? |
The new templates are horrible for beginners. I have been literally using Blazor since it was in beta and I still had trouble wrapping my head around them. Let's assume you are a beginner to Blazor and want to try it out or evaluate it. With the new project you created you will realize sooner or later that:
All of these problems did not exist in .Net 7.0 templates. You could just create a new project and get going. To make the new template behave like the old version, here are the steps you have to take:
// App.razor - Server project
@code {
//...
private IComponentRenderMode? PageRenderMode => HttpContext.AcceptsInteractiveRouting() ? InteractiveWebAssemblyNoPrerender : null;
private static readonly IComponentRenderMode InteractiveWebAssemblyNoPrerender = new InteractiveWebAssemblyRenderMode(prerender:false);
}
// Why is prerendering on by default? It adds complexity and has so many considerations
// and technical challenges that are detrimental to a beginners experience. This should ALWAYS be an opt-in.
// Also why are there no static RenderMode properties with prerendering disabled?
// Why do I have to create it myself? It makes it hard to discover what you have to do to disable prerendering.
// Just add them to the static RenderMode class. (e.g. RenderMode.InteractiveWebAssemblyNoPrerender)
// Loader.razor - Client project
// This has to be in the client project to be visible in both server and client.
@if (!this.RendererInfo.IsInteractive)
{
// Add whatever loading UI you want.
<p>Loading ...</p>
}
// App.razor - Server Project
//...
<body>
@if (HttpContext.AcceptsInteractiveRouting())
{
<Loader @rendermode="InteractiveWebAssembly" />
}
<Routes @rendermode="PageRenderMode" />
<script src="_framework/blazor.web.js"></script>
</body>
//...
// You need the conditional render so that the loader does not render for the Account management pages.
// Another beginner's trap.
// Program.cs - Client project
builder.Services.AddScoped(sp => new HttpClient()
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
// Why is this not included by default?
// You will 100% need to talk to the fricking server you just created.
// Also how is a beginner supposed to know how to set the BaseAddress properly?
// Program.cs - Server project
//...
builder.Services.AddControllers();
//...
app.MapControllers();
//...
// Again why is this not added by default?
// What is the point of a client-server app if the client cannot talk to the server?
// Sure you may prefer to use minimal APIs instead, but the template has nothing enabled. After all these steps you now have an app that behaves mostly the same as the previous Blazor Wasm hosted template. Sure there may be other ways to implement some things (e.g. add a loader as root component from the client Program.cs??) but the point is that this is an incomplete template. Even the Weather forecast sample is faked. Why ?? I am an experienced Blazor developer and I still forget some of these steps, I have to keep other sample projects around to remember what to do. There is no excuse for the templates to be incomplete. It is as if the people that created them have never tried to use them (to create an app, not simply run the counter page). A bad template can drive people away, it is their first interaction with a new ecosystem. This should have been fixed in .Net 9.0. |
This.
Sounds very likely.
This is exactly what I do. The new universal template is so bad (I don't think that any variant of it creates you a
Absolutely. The first interaction is very, very important. And currently, that interaction is very bad. If adoption rates fall, I honestly think that Microsoft would only have themselves to blame - for completely forgetting about new users. What attracted me to Blazor - and what keeps me hooked - is the ability to write my in-browser code in C#, and not JS/TS. I don't have anything against JS/TS/React et al, but what I really do object to is the requirement to install Node and npm and Node modules (the horror) for just about any client-side JS framework that appeared after Angular 1. (Certainly that was the situation when I adopted Blazor WebAssembly in 2020.) The .NET 8+ rendering modes etc. are irrelevant to me. Nice to have, maybe, but in introducing them, along with the appalling Universal Template, you threw out the baby with the bathwater: i.e. the ability to simply and quickly create a new Blazor WebAssembly project with a Client, Server, Shared project structure and the ability for the client to get data from the server via shared data models. From 2020 to November 2023, I recommended Blazor WebAssembly to all and sundry for SPA-type web apps. But right now, it's hard to recommend it for any new user. Just look at the steps that @stavroskasidis has indicated that a new user would have to take... it's too big an ask. |
I would love to see this template for the other rendermodes as well. In addition I would love to see JWT based authentification support out of the box for the template. |
Like some others in this thread I have been using Blazor since the earliest previews and have ALWAYS thought that there is undue handwaving away of the huge difference between "Blazor Server" and "Blazor WebAssembly" (I may not have kept up with the current names) when it comes to the whole business about transferring data between the client and server. People seem to get transfixed by the fact that the top of the UI code can be common and completely overlook that one system is going to be getting its data simply by typing The nadir of this is reached in the form of this template, where all the code that would give the lie to the false notion that all flavours are the same has simply been omitted. |
@willdean - it took me a while to understand what you're saying. I had to read your post a few times, and parse it very carefully to get it, but yeah: I understand what you're saying and I agree. You shed a whole new light on why the current new-project template is as it is, potentially. I.e. why (possibly) it doesn't bother to show how the I re-read Steve's "Note for community: Recommendation for .NET 8 developers" at the top of this ticket just now. He concludes "At that point your UI is just running in the browser and you can make HttpClient calls to the server as normal.". This is fine if you adopted pre-November 2023. But for new adopters, there is no "normal" in that regard, and the current new-project template won't establish that "normal". Because this ticket (in the title) only calls for re-adding "proper API calls" (which is totally needed) and because it's backlogged and doesn't even have .NET 10 Milestone, I've opened a new ticket to address the current new-project template's drawbacks more generally (and I know you've seen it @willdean). |
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: