Blazor 应用程序作为 WASM 或服务器运行。无法让依赖注入在服务器模式下工作

Posted

技术标签:

【中文标题】Blazor 应用程序作为 WASM 或服务器运行。无法让依赖注入在服务器模式下工作【英文标题】:Blazor App Run as WASM or Server. Cannot get Dependency Injection working in Server Mode 【发布时间】:2021-09-22 12:21:05 【问题描述】:

我已经阅读了大量关于在单个应用程序中切换 WASM 和服务器模式的文章。最符合我要求的是:https://itnext.io/blazor-switching-server-and-webassembly-at-runtime-d65c25fd4d8

我正在尝试设置我的项目,以便由于更好的调试等而可以在服务器模式下开发,并使用 WebAssembly 进行部署。为了隔离我一直面临的问题,我基于标准的 WebAssembly(托管)模板创建了这个 github 存储库。 https://github.com/gwruck/Blazor_WASM_Server。有关详细信息,请参阅自述文件。

该解决方案在 WebAssembly 模式下运行良好,但在服务器模式下,我收到错误消息:“InvalidOperationException:无法为类型 'Blazor_WASM_Server.Client.Pages.Index' 的属性 'api' 提供值。没有注册服务'Blazor_WASM_Server.Client.WebApiClient' 类型。

我在客户端应用程序 (WebApiClient) 中创建了一个 Typed Http 客户端并将其注入到索引页面中。我也尝试过注入许多其他服务,但我似乎无法让它们中的任何一个在服务器模式下工作。

根据这篇文章,我认为这种方法可行。 https://www.pragimtech.com/blog/blazor/call-rest-api-from-blazor/

我知道 WASM 和 Server 对 DI 的处理是不同的,但在这种情况下我根本无法让它工作。这是尝试组合两种 Blazor 的基本限制,还是有人能找到解决方法?如果您需要更多信息来描述问题,请告诉我。

以下是代码的关键元素:

Blazor_WASM_Server.Server

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.0-preview.5.21301.17" />
    <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-preview.5.21301.5" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\Client\Blazor_WASM_Server.Client.csproj" />
    <ProjectReference Include="..\Shared\Blazor_WASM_Server.Shared.csproj" />
  </ItemGroup>
</Project>

namespace Blazor_WASM_Server.Server

    public class Program
    
        public static void Main(string[] args)
        
            CreateHostBuilder(args).Build().Run();
        

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                
                    webBuilder.UseStartup<Startup>();
                );
    

   public enum HybridType
    
        ServerSide,
        WebAssembly//,
    
    public class HybridOptions
    
        //Choose WebAssembly or ServerSide
        public HybridType HybridType  get; set;  = HybridType.ServerSide;
    
public class Startup

    public Startup(IConfiguration configuration)
    
        Configuration = configuration;
    

    public HybridOptions hybridOptions = new HybridOptions();

    public IConfiguration Configuration  get; 

    public void ConfigureServices(IServiceCollection services)
    
        services.AddControllersWithViews();
        services.AddRazorPages();
           

        if (hybridOptions.HybridType == HybridType.ServerSide)
        
            //Conditionally add ServerSide
            services.AddServerSideBlazor();//new
        
        services.Configure<HybridOptions>(Configuration);
    
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    
        if (env.IsDevelopment())
        
            app.UseDeveloperExceptionPage();
            app.UseWebAssemblyDebugging();
        
        else
        
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        

        app.UseHttpsRedirection();
        app.UseBlazorFrameworkFiles();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        
            endpoints.MapRazorPages();
            endpoints.MapControllers();
            endpoints.MapFallbackToPage("/_Host");
        );
    


_Host.cshtml

@page "_Host"
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@using Blazor_WASM_Server.Server
@using Microsoft.Extensions.Options
@using Blazor_WASM_Server.Client

@inject IOptions<HybridOptions> HybridOptions
@
    @*Retrieve the running mode*@
    var hybridType = HybridOptions?.Value?.HybridType ?? HybridType.WebAssembly;


<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Sabre PM</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="css/site.css" rel="stylesheet" />
    <link href="_content/Syncfusion.Blazor.Themes/bootstrap4.css" rel="stylesheet" />
</head>
<body>
<div id="app">
    **@*Conditionally apply Server/Webassembly.  This is where the magic happens*@**
    @if (hybridType == HybridType.ServerSide)
    
        <component type="typeof(App)" render-mode="ServerPrerendered"/>
        <persist-component-state/>
        <script src="_framework/blazor.server.js"></script>
    
    else if (hybridType == HybridType.WebAssembly)
    
        <div id="app">Loading...</div>
        <script src="_framework/blazor.webassembly.js"></script>
    

</div>
    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">????</a>
    </div>
</body>
</html>

Blazor_WASM_Server.Client

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0-preview.5.21301.17" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0-preview.5.21301.17" PrivateAssets="all" />
    <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-preview.5.21301.5" />
    <PackageReference Include="System.Net.Http.Json" Version="6.0.0-preview.5.21301.5" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\Shared\Blazor_WASM_Server.Shared.csproj" />
  </ItemGroup>
</Project>

 public class Program
    
        public static async Task Main(string[] args)
        
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<Client.App>("#app");

            //Added a typed Http client
            builder.Services.AddHttpClient<WebApiClient>(client =>    
            
                client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(
                    new MediaTypeWithQualityHeaderValue("application/json"));
            );

            builder.Services.AddScoped(sp => new HttpClient  BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) );

            await builder.Build().RunAsync();
        
    

public class WebApiClient

    public HttpClient Client  get; 

    public WebApiClient(HttpClient client)
    
        Client = client;
    


@* Index.razor *@
@page "/"

@inject WebApiClient api

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

@code

    //Inject the WebApiClient and verify it has been set
    protected override void OnInitialized()
    
         **//For some reason, whatever I try to inject here will work in WASM Mode, but not in SeverSide mode**
        if (api == null) throw new NullReferenceException("WebApiClient not set by Dependency Injection");

        base.OnInitialized();
    


我知道这经常混合和匹配范例,但如果您将其设置为读取 Web API,则非常相似的代码将在“纯”服务器端应用程序中运行。我只是无法弄清楚为什么它在这个混合版本中不起作用。我怀疑这与项目类型有关(Microsoft.NET.Sdk.BlazorWebAssembly vs Microsoft.NET.Sdk.Web

我意识到这是一个“大”问题,但希望这能将其提炼为关键要素。我怀疑我忽略的架构中缺少一些基本的东西。

【问题讨论】:

当服务器模式出现问题时,发布该代码更有意义。 Startup.cs 和 Program.cs 开始。 【参考方案1】:

在查看您的回购后更新

您需要在 Blazor 服务器启动中注册一个 HttpClient

Blazor_WASM_Server.Server 中的 startup.cs 应如下所示:

using Blazor_WASM_Server.Client;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Linq;
using System.Net.Http;

namespace Blazor_WASM_Server.Server

    public enum HybridType
    
        ServerSide,
        WebAssembly//,
        //HybridManual,
        //HybridOnNavigation,
        //HybridOnReady
    
    public class HybridOptions
    
        public HybridType HybridType  get; set;  = HybridType.ServerSide;
    

    public class Startup
    
        public Startup(IConfiguration configuration)
        
            Configuration = configuration;
        

        public HybridOptions hybridOptions = new HybridOptions();

        public IConfiguration Configuration  get; 

        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        

            services.AddControllersWithViews();
            services.AddRazorPages();



            if (hybridOptions.HybridType == HybridType.ServerSide)
            
                services.AddServerSideBlazor();
                // Server Side Blazor doesn't register HttpClient by default
                // Thanks to Robin Sue - Suchiman https://github.com/Suchiman/BlazorDualMode
                if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
                
                    // Setup HttpClient for server side in a client side compatible fashion
                    services.AddScoped<HttpClient>(s =>
                    
                        // Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
                        var uriHelper = s.GetRequiredService<NavigationManager>();
                        return new HttpClient
                        
                            BaseAddress = new Uri(uriHelper.BaseUri)
                        ;
                    );
                
                services.AddScoped<WebApiClient>();
            
            services.Configure<HybridOptions>(Configuration);
        

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        
            if (env.IsDevelopment())
            
                app.UseDeveloperExceptionPage();
                app.UseWebAssemblyDebugging();
            
            else
            
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            

            app.UseHttpsRedirection();
            app.UseBlazorFrameworkFiles();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            
                endpoints.MapRazorPages();
                endpoints.MapControllers();
                endpoints.MapFallbackToPage("/_Host");
            );
        
    

您可以在 https://github.com/ShaunCurtis/AllinOne 上查看我对合并网站的看法,

【讨论】:

哇 - 那是我错过的金块。我看过 Robin Sue 的作品,但必须承认我错过了那部分。我以前没见过你的 - 现在会研究它。希望这个帖子可以帮助其他人避免我刚刚落下的兔子洞!!! NP。 :-) 进入黑洞并朝自己的脚开枪是编码中的常见做法! 再次感谢您。我“意识到” WASM 在浏览器中运行,而 Server 在服务器上运行,但我忘记了在何处注册服务的微妙之处——即使经过数小时的研究。我可以从 MS 获得的最接近的是在这个链接上,但它并没有真正深入到它的底部,恕我直言。 link.

以上是关于Blazor 应用程序作为 WASM 或服务器运行。无法让依赖注入在服务器模式下工作的主要内容,如果未能解决你的问题,请参考以下文章

使用身份验证托管的 Blazor Wasm 上出现 Azure 500 错误

Blazor WASM 视图模型

使用 IIS URL 重写动态设置 Blazor WASM 基本 href

如何更改 blazor wasm 应用程序的基本 URL

blazor wasm 如何从 di 注入自定义类

如何绕过 blazor wasm 路由系统并调用服务器