Skip to content
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

Open
SteveSandersonMS opened this issue Apr 23, 2024 · 16 comments
Open

Re-add proper API calls into Blazor WebAssembly template #55307

SteveSandersonMS opened this issue Apr 23, 2024 · 16 comments
Assignees
Labels
area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-templates
Milestone

Comments

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Apr 23, 2024

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:

  • Deal with prerendering or auto mode. In these cases, you have to be able to load the data when running on WebAssembly and when running on the server. So you must either:
    • Register HttpClient in DI with a base address on both server and client, and have the server call itself via HTTP
    • Or, have some interface representing the datastore, and set up both server and client implementations
  • Avoid the double-render problem whereby the data is loaded twice (once for prerendering, once for interactivity)
    • This is more complicated with per-page interactivity, since enanced nav doesn't currently work with persistent state
  • Make this work sensibly with auth, including for both client and server data access

While 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., in App.razor), replace it with new 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.

@SteveSandersonMS SteveSandersonMS added the area-blazor Includes: Blazor, Razor Components label Apr 23, 2024
@mrpmorris
Copy link

mrpmorris commented Apr 23, 2024

Thanks for adding this!

I would like to propose this is two separate issues.

  1. Double-render
  2. WASM vs Server-side rendering of data using the same page

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

  1. A new "Contracts" project that contains the DTOs used by the web api, and also an interface for fetching that data, e.g. IWeatherForecastService
  2. A class implementing said interface is added to the WASM app and is simply a proxy to make HttpClient calls through to the API server endpoint.
  3. The server registers a class that implements said interface by (simulating) fetching data from a database (Task.Delay).
  4. The server API endpoint uses that service to fetch data and return it to the client.

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

  1. The I{Weather}Service naming pattern is well-known, and easy to understand for those who've never seen it before.
  2. The client's implementation merely acts as a proxy through to the server service (via an API call), similar to good old RPC.
  3. The page works on both WASM and Server-Side rendering without modification to the Razor page.
  4. When server rendering, the server doesn't have to call itself via HTTP - instead it executes the same code that the API endpoint would have, which is obviously faster.

MyHostedBlazorWasmApp.zip

@EdCharbeneau
Copy link

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.

@mkArtakMSFT mkArtakMSFT added the enhancement This issue represents an ask for new feature or an enhancement to an existing one label Apr 23, 2024
@Hona
Copy link
Contributor

Hona commented Apr 24, 2024

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

@mrpmorris
Copy link

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 x.TryTakeFromState for each is laborious. I would like to propose being able to write (something like) the following code instead.

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();
        }
    }
    // ...
}
  1. All public fields and properties (with getter + setter) are automatically serialized and deserialized (presumably excluding parameters/cascading values as these will be set by outer components and passed in).
  2. Any of those can be excluded from serialization if decorated with [DoNotSerializeState].
  3. Lower visibility members can be included in serialized if decorated with [SerializeState].
  4. Introduce an overloaded OnInitializedAsync that has a bool parameter indicating whether or not the component state was deserialized, by default this just calls OnInitializedAsync (which can possibly be marked [Obsolete] in V10 and removed in V11?)
  5. All components are automatically opt-in unless the coder of the component explicitly marks their component or assembly with [DoNotSerializeState].

I think this gives the easiest experience for new users and also allows more experienced users to tweak more precisely if needed.

  1. Automatically deals with serializing state from the server pre-render down to the WASM client to avoid double-rendering.
  2. The the user must explicitly write code to not fetch data when deserializing state, so existing code bases will continue to work.
  3. The default implementation of the new overloaded OnInitializedAsync can call OnInitializedAsync, so no code breakage.
  4. In apps where 99% of pages are accessed only via WASM navigation and not server pre-rendering, it will have no impact at all.

Possible alternative names for this parameter hasPreRenderedState. It would be nice if the meaning could be negated in order to avoid the ! in the if, but I can't think of a good name. Chat-GPT (Cheat-Petey) suggests stateIsUninitialized - which I think works quite well.

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 override on OnInitializedAsync - the OnInitialized would have to be implemented by Roslyn, determine if there is state to deserialize, and then call a new partial OnInitializedAsync with the new parameter. But I don't know if that's a requirement or not.

@mkArtakMSFT mkArtakMSFT added this to the 9.0-preview5 milestone Apr 29, 2024
@mkArtakMSFT mkArtakMSFT self-assigned this May 1, 2024
@Hona
Copy link
Contributor

Hona commented May 2, 2024

@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

  • ASP.NET Core host
  • Layout, Pages, Components (note, this is the only place to write Razor files)
  • Contracts: Server implementation (e.g. call EF Core directly)

Contoso.Contracts

  • View Model definitions
  • Data access interfaces

Contoso.Client

  • Contracts: Clientside implementation (e.g. call an authenticated Web API)

Now, what would this look like?

Contoso/Components/WeatherPage.razor

@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();
        }
    }
}

Contoso.Contracts/Models/WeatherForecast.cs

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.

Contoso/Repositories/WeatherRepository.cs

public class WeatherRepository(AppDbContext DbContext) : IWeatherRepository
{
    public Task<WeatherForecast[]> GetForecastsAsync()
    {
         // Simulating some sort of EF query
         return await DbContext.Forecasts
             .ToListAsync();
    } 
}

Contoso.Client/Repositories/WeatherRepository.cs

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

Contoso.Client/Program.cs (I'd prefer something like DependencyInjection.cs or something that shows this is only for wiring up contract interfaces to implementations)

...
builder.Services.AddSingleton<IWeatherRepository, WeatherRepository>();
...

Contoso/Program.cs

...
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 don't like the full sharding of 3 projects out of the gate, we could move code to be a bit closer
    I do think the sharding & abstractions come at a cost, so I think it needs some balancing.
  • I didn't put too much thought in the abstractions naming, e.g. repository/service etc.
  • I didn't consider how configured HttpClients could be convenient for consumers

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.
A fresh load is inside an if, or past a guard clause, then deserialization cleanup/handling is next to it.

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:

  • the default template has 1 project somehow than my suggestions (following a bit more of a code locality structure)
  • the pattern is established for data access & reuse between client/server
  • a single spot for razor pages/components (my samples would mean you only write that inside the Contoso server project
  • beginners are shown the path, and experts are allowed to have control (opt in)

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.
The foundation is there for the new methods, but let's make it easy to use.

Keen to hear everyone's thoughts :)

@MackinnonBuck
Copy link
Member

MackinnonBuck commented Jun 17, 2024

Update

After 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:

  • We don't feel the changes described in this issue are an obvious net improvement to the template
  • We only have one chance to get these changes right; we don't have time to react to additional customer feedback given the stage we're currently at in the release cycle

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 changes

The original proposal was to make the following changes to variations of the Blazor Web template that use WebAssembly interactivity only:

  • Replace usage of RenderMode.InteractiveWebAssembly with new InteractiveWebAssemblyRenderMode(prerender: false)
  • Add an endpoint on the server to provide weather forecast data
  • Make the following changes to the Weather page:
    • Disable prerendering and streaming
    • In the per-page interactivity case, add @rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
    • Inject an HttpClient and use it to fetch weather forecast data in OnInitializedAsync()

The following sections describe some tradeoffs we considered when evaluating these changes.

Tradeoff 1: UX regression

One 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 indicator

A 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 component:

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:

  1. Prerendering the loading indicator markup onto the page. The loading indicator styling gets updated by CSS variables that reflect the WebAssembly loading progress.
  2. After interactivity starts, the component stops rendering the loading indicator to allow other interactive content to take its place.

When using global WebAssembly interactivity, we can simply plop the <LoadingIndicator> in App.razor. However, for the per-page interactivity case, things get a little more tricky; we only want to render the loading indicator on pages that utilize WebAssembly interactivity (with prerendering disabled).

One way to achieve this is to move MainLayout.razor and NavMenu.razor to the .Client project and introduce a WebAssemblyLoadingLayout component that renders a loading indicator alongside the body.

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:

  • It sometimes results in a flicker when navigating to the weather page after the WebAssembly runtime initializes:
    • This happens because:
      1. The loading indicator gets prerendered onto the screen
      2. The component becomes almost instantly interactive, removing the loading indicator
    • There are various ways to fix this, like add a fade-in animation on the loading indicator and make it not appear for the first 100ms or so.
  • It results in a broken UI if you enable prerendering and have the WebAssemblyLoadingLayout
    • i.e., we introduce a new way to misconfigure the UI of your app
  • It requires moving layout components into the .Client project (so that WebAssemblyLoadinglayout can inherit from MainLayout)

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 WebAssemblyLoadingLayout, we could update the existing MainLayout and keep it in the server project:

@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 prerendering

We also briefly considered whether there was a way to cleanly make the Weather page prerendered but still use HttpClient to fetch data after interactivity starts. We even briefly considered whether a new framework feature could help with this. For example:

@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 interactivity

The 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 HttpClient would be helpful, but we don't have a clear sense of what the magnitude of the opposite feedback would be; how many developers would ask that we revert to using stream rendering in the template?

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 HttpClient.

Requested feedback

To 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!

@SteveSandersonMS
Copy link
Member Author

SteveSandersonMS commented Jun 18, 2024

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:

  • Render the WebAssembly loading indicator unconditionally from MainLayout.razor (just by inlining the <svg> alongside the @Body output), no matter the page render mode or global interactivity choice
  • Update the framework JS code so that, when it sets --blazor-load-percentage, it also applies a new CSS class blazor-webassembly-loading to the document element, and removes it once loading is completed. That is, <html> will have this class if and only if loading is currently in progress.
  • In app.css, add html:not(.blazor-webassembly-loading) .loading-progress { display: none }

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.

@MackinnonBuck
Copy link
Member

MackinnonBuck commented Jun 18, 2024

@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:

  • Even after loading completes, there's still more WebAssembly runtime initialization that needs to happen before the first render
  • When navigating between pages, enhanced nav removes the "style" attribute from the html element, so we'd have to constantly reapply it (I guess it's not a huge deal, just requires some more code in the product).

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:

  • The loading indicator displays even for pages that use Auto or Server interactivity without prerendering, which is odd.
  • The loading CSS variables still don't get applied to the html element until after blazor.web.js loads and starts downloading resources, so the slowest connections still won't get the loading indicator right away.

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!

@AlbertoPa
Copy link

AlbertoPa commented Jun 26, 2024

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:

  1. App with local user accounts in the three render modes (already available), with a realistic example of an API call (currently missing: there is a database already using EF Core, why not make a trivial model/query/api call instead than hardcoding data etc.?).

  2. App with Entra using the Microsoft Identity Web library (currently missing, some samples available using generic OIDC, which could also be an option for third-party OIDC providers) demonstrating the different render modes and API calls (already in the works - there is an issue open about it).

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.

@DjeeBay
Copy link

DjeeBay commented Jul 15, 2024

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.
Back in .Net 7 the templates were ready to go.

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 :

  1. WASM : like .Net 7 with an API call and a Shared project that shows the real benefit of Blazor : sharing code between front-end and back-end
  2. Server : all is rendered backend with a WS connection
  3. Auto (Advanced) : whatever sample you want, but this mode adds a warning to notice developers that it needs advanced knowledge and demonstrates the powerful things you can do in an advanced mode.

@Bellarmine-Head
Copy link

Bellarmine-Head commented Jan 3, 2025

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:-

  • Client
  • Server
  • Shared

and showed me the all-important how-the-client-gets-data-from-the-server technique.

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 getting-data-from-the-server technique in this ticket.

@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.

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.

^^ 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?

@stavroskasidis
Copy link

stavroskasidis commented Jan 3, 2025

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:-

  • Client
  • Server
  • Shared

and showed me the all-important how-the-client-gets-data-from-the-server technique.

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 getting-data-from-the-server technique in this ticket.

@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.

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.

^^ 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.
You heard that there are two Blazor flavors (server/wasm) and you decide to go for the wasm one.
So you try to create a new Blazor Web app with authentication using the new template. You don't know or care about automatic render modes, interactivity and all that new bells and whistles. You just want to create a new project and start writing code.

With the new project you created you will realize sooner or later that:

  • You have no sample of how to communicate with the server.
  • You have no sample of how dependecy injection works in components.
  • You get exceptions if you inject services to your components because you have no idea that prerendering is on by default and you need to register services twice.
  • You have to add specific initialization handling in every page because prerendering causes everything to be called twice, so more hidden complexity.
  • HttpClient is not registered in the client project.
  • The server project has no Controllers and they are not mapped in Program.cs.

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:

  1. Create an new Blazor Web App and set the proper options in the template (Auth type: Individual Accounts, Interactive Mode: Wasm, Interactivity: Global).
  2. Disable prerendering by default in the server project
// 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)
  1. Create a Loader component so that your page does not appear empty while Blazor wasm is loading.
// 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>
}
  1. Add the loader component to the App.razor file.
// 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.
  1. Register the HttpClient in the client project.
// 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?
  1. Add controllers support in the Server project
// 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.

@Bellarmine-Head
Copy link

Bellarmine-Head commented Jan 3, 2025

All of these problems did not exist in .Net 7.0 templates. You could just create a new project and get going.

This.

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).

Sounds very likely.

I have to keep other sample projects around to remember what to do.

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 Shared project, for instance) that I have to clone an existing solution (created with the 2020 template, but modified to fit how Blazor WebAssembly works in .NET 8) to start a new one.

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.

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.

@MarvinKlein1508
Copy link

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.

@willdean
Copy link

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 =object.member and the other is going to be in a vast ocean of HttpClient calls, JSON serialization, API controllers and JWT tokens (and probably DTO classes). There are (or used to be) official guidelines that components should be written not to care where they run. If that's true for any components at all, is only a very specific subset.

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.

@Bellarmine-Head
Copy link

Bellarmine-Head commented Jan 16, 2025

@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 Client project (= browser) gets data from the Server (= web server) - and that's because of a enormous level of denial about the huge difference between the two, in practical terms.

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-templates
Projects
None yet
Development

No branches or pull requests