DependencyInjection.StaticAccessor.Blazor 8.1.0-preview-1734072808

This is a prerelease version of DependencyInjection.StaticAccessor.Blazor.
There is a newer version of this package available.
See the version list below for details.
dotnet add package DependencyInjection.StaticAccessor.Blazor --version 8.1.0-preview-1734072808                
NuGet\Install-Package DependencyInjection.StaticAccessor.Blazor -Version 8.1.0-preview-1734072808                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DependencyInjection.StaticAccessor.Blazor" Version="8.1.0-preview-1734072808" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add DependencyInjection.StaticAccessor.Blazor --version 8.1.0-preview-1734072808                
#r "nuget: DependencyInjection.StaticAccessor.Blazor, 8.1.0-preview-1734072808"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install DependencyInjection.StaticAccessor.Blazor as a Cake Addin
#addin nuget:?package=DependencyInjection.StaticAccessor.Blazor&version=8.1.0-preview-1734072808&prerelease

// Install DependencyInjection.StaticAccessor.Blazor as a Cake Tool
#tool nuget:?package=DependencyInjection.StaticAccessor.Blazor&version=8.1.0-preview-1734072808&prerelease                

DependencyInjection.StaticAccessor

中文 | English

DependencyInjection.StaticAccessor is dedicated to providing static access to the IServiceProvider object corresponding to the current DI Scope for various types of .NET projects. This allows you to easily use IServiceProvider in static methods and in types where direct interaction with DI services is not possible.

Why Do We Need PinnedScope

When working with dependency injection, a common challenge is accessing the IoC/DI container within static methods. There are many solutions to this problem, and the simplest is to save the root container to a static variable during application startup and access it directly afterward:

public static IServiceProvider ServiceProvider;

static void Main(string[] args)
{
    var builder = Host.CreateDefaultBuilder();

    var host = builder.Build();

    ServiceProvider = host.Services;

    host.Run();
}

This approach is straightforward and effective for simple scenarios. However, we know that Microsoft Dependency Injection offers three service lifetimes: Transient, Scoped, and Singleton. Among them, Scoped is particularly special. In web development, a Scoped service corresponds to the lifetime of a single request. For services registered with the Scoped lifetime, we cannot retrieve them directly from the root container; they must be accessed through a scope-specific container.

For this requirement in web applications, there is an easy solution. Microsoft provides the IHttpContextAccessor, which allows us to obtain the current request's IServiceProvider via IHttpContextAccessor.HttpContext.RequestServices after retrieving the IHttpContextAccessor object from the root container.

Why Is PinnedScope Necessary?

Despite the existence of these straightforward solutions, scope management becomes more complex in advanced scenarios. For instance, in web applications, you might need to launch a background thread to complete some asynchronous operations, allowing the request to proceed without waiting for the operation to finish. In such asynchronous operations, DI containers are often required. How can we access the DI container in a static method in this case? Can we continue using the root container or IHttpContextAccessor? The answer is no, because by this point, the request may have already been completed, and accessing IServiceProvider through IHttpContextAccessor will result in an exception, as it may reference a disposed service provider.

In such cases, you usually need to create a new scope specifically for the asynchronous operation. While you can manage these scopes manually, PinnedScope offers a simpler alternative.

The above is just one example. In reality, scope management can be even more complicated in scenarios like task scheduling, message subscriptions, and even Microsoft's official Blazor framework. While you could find tailored solutions for each scenario to access the IServiceProvider, PinnedScope provides a unified and simple approach. With PinnedScope, you don't need to worry about how your framework creates and manages scopes—you can always retrieve the correct IServiceProvider effortlessly.

NuGet Packages Overview

Package Name Purpose
DependencyInjection.StaticAccessor.Hosting For AspNetCore projects (WebApi, Mvc, etc.) and Generic Host
DependencyInjection.StaticAccessor.Blazor For Blazor projects, including Blazor Server and Blazor WebAssembly Server
DependencyInjection.StaticAccessor.Blazor.WebAssembly For Blazor WebAssembly Client projects, also supports Auto mode for Client projects
DependencyInjection.StaticAccessor Base library, A non-startup project using PinnedScope references the package

Version Number Explanation

All version numbers follow the Semantic Versioning format. The major version matches the version of Microsoft.Extensions.* ( please make sure the major version matches the Microsoft.Extensions.* version you are using when referencing the NuGet package). The minor version indicates feature updates, while the patch version is for bug fixes.

Quick Start

Install the corresponding NuGet package based on your project type, as listed in the NuGet Packages Overview.

// 1. Initialization (Generic Host)
var builder = Host.CreateDefaultBuilder();

builder.UsePinnedScopeServiceProvider(); // Only this step to complete initialization

var host = builder.Build();

host.Run();

// 2. Access anywhere
class Test
{
    public static void M()
    {
        var yourService = PinnedScope.ScopedServices.GetService<IYourService>();
    }
}

The initialization method is similar for different project types, and all require calling the UsePinnedScopeServiceProvider extension method. Example initialization code for different project types will be provided later.

AspNetCore Project Initialization Example

Install the NuGet package

dotnet add package DependencyInjection.StaticAccessor.Hosting

// 1. Initialization
var builder = WebApplication.CreateBuilder();

builder.Host.UsePinnedScopeServiceProvider(); // Just this step to complete the initialization

var app = builder.Build();

app.Run();

Blazor Server-Side Project Initialization

Note: The Blazor Server-side project discussed here includes Server, WebAssembly, and Auto modes, not just the Server mode specifically.

Install NuGet Package

Run the following command to install the required package:

dotnet add package DependencyInjection.StaticAccessor.Blazor

The initialization process for Blazor Server is similar to that of an AspNetCore project. Refer to the AspNetCore Project Initialization Example for detailed instructions. However, since Blazor's DI scope differs from the standard AspNetCore DI scope, additional steps are required.

Pages Inherit from PinnedScopeComponentBase

Blazor's unique DI scope requires that all pages inherit from PinnedScopeComponentBase. It is recommended to define this inheritance globally in the _Imports.razor file so that it applies to all pages.

// _Imports.razor

@inherits DependencyInjection.StaticAccessor.Blazor.PinnedScopeComponentBase

In addition to PinnedScopeComponentBase, the library provides PinnedScopeOwningComponentBase and PinnedScopeLayoutComponentBase, with the possibility of adding more types as needed in the future.

Special Note
By default, when a page inherits from PinnedScopeOwningComponentBase, accessing PinnedScope.ScopedServices within callback methods or subsequent method calls retrieves the same object as [Inject]'s IServiceProvider. This behavior may differ from the expected OwningComponentBase.ScopedServices. To achieve the expected behavior, after calling UsePinnedScopeServiceProvider during initialization, call the UseOwningScopedServices extension method as shown below.

Example Initialization and Page Usage
// Main
var builder = WebApplication.CreateBuilder(args);

// Add required services and configurations here...

builder.Host
    .UsePinnedScopeServiceProvider()  // Initializes PinnedScope
    .UseOwningScopedServices();       // Ensures PinnedScope.ScopedServices maps to OwningComponentBase.Services in inherited pages

var app = builder.Build();

// Configure the middleware pipeline...

app.Run();
Sample Razor Page: Test.razor
@page "/test"
@using DependencyInjection.StaticAccessor
@using DependencyInjection.StaticAccessor.Blazor
@rendermode InteractiveServer
@inject IServiceProvider serviceProvider
@inherits PinnedScopeOwningComponentBase

<PageTitle>Test</PageTitle>

<button class="btn btn-primary" @onclick="Call">Click</button>

@code {
    private void Call()
    {
        var equals1 = serviceProvider == PinnedScope.ScopedServices;
        var equals2 = ScopedServices == PinnedScope.ScopedServices;

        /**
         * Results based on initialization:
         * 1. Without calling UseOwningScopedServices:
         *     equals1: true, equals2: false
         *
         * 2. With UseOwningScopedServices called during initialization:
         *     equals1: false, equals2: true
         */
    }
}

By following this setup, the behavior of PinnedScope.ScopedServices will align with your expectations, ensuring compatibility with Blazor's scoped DI features while offering flexibility for different inheritance scenarios.

Solution for Existing Custom ComponentBase Base Classes

If you are already using a custom ComponentBase base class provided by another package, C#'s lack of multiple inheritance can pose a challenge. Here are the recommended solutions:

  1. If you can modify your base class, and it directly inherits ComponentBase, OwningComponentBase, or LayoutComponentBase

    Replace the base class with PinnedScopeComponentBase, PinnedScopeOwningComponentBase, or PinnedScopeLayoutComponentBase, depending on your use case.

  2. If you can modify your base class, but it does not directly inherit from ComponentBase, OwningComponentBase, or LayoutComponentBase

    Modify your base class to implement the IHandleEvent and IServiceProviderHolder interfaces. Implement the interface methods based on the corresponding PinnedScope base class implementations.

  3. Unable to modify your base class

    Create a new custom base class that inherits from your existing base class, implements the IHandleEvent and IServiceProviderHolder interfaces and implements the required methods by following the corresponding PinnedScope base class implementations.

Blazor WebAssembly Client Initialization

Note: This is for Blazor WebAssembly Client-side initialization. For Server-side initialization, please refer to Blazor Server-Side Project Initialization

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.UsePinnedScopeServiceProvider(); // Just this step to complete the initialization

await builder.Build().RunAsync();

Similar to the Server-side, Client-side pages also need to inherit from PinnedScopeComponentBase. Please refer to Inherit PinnedScopeComponentBase for Pages.

Notes

Do Not Operate IServiceScope Through PinnedScope

Although you can access the current DI Scope via PinnedScope.Scope, please do not directly manipulate PinnedScope.Scope, such as calling the Dispose method. Instead, you should operate through the variable created when you initially created the scope.

Does Not Support Non-Standard Scopes

In typical development scenarios, this issue usually does not need to be addressed, and it generally does not occur in standard AspNetCore projects. However, Blazor is an example of a non-standard DI Scope in official project types.

Before explaining what a non-standard Scope is, let's first talk about the standard Scope mode. We know that DI Scopes can be nested, and under normal circumstances, the nested Scopes form a stack structure, where the most recently created Scope is released first, in an orderly manner.

using (var scope11 = serviceProvider.CreateScope())                    // push scope11. [scope11]
{
    using (var scope21 = scope11.ServiceProvider.CreateScope())        // push scope21. [scope11, scope21]
    {
        using (var scope31 = scope21.ServiceProvider.CreateScope())    // push scope31. [scope11, scope21, scope31]
        {

        }                                                              // pop scope31.  [scope11, scope21]

        using (var scope32 = scope21.ServiceProvider.CreateScope())    // push scope32. [scope11, scope21, scope32]
        {

        }                                                              // pop scope32.  [scope11, scope21]
    }                                                                  // pop scope21.  [scope11]

    using (var scope22 = scope11.ServiceProvider.CreateScope())        // push scope22. [scope11, scope22]
    {

    }                                                                  // pop scope22.  [scope11]
}                                                                      // pop scope11.  []

With this understanding of standard Scopes, non-standard Scopes can be easily understood. Any Scope that does not follow this orderly stack structure is considered a non-standard Scope. A common example is the scenario with Blazor:

As we know, Blazor SSR implements SPA through SignalR, where each SignalR connection corresponds to a DI Scope. Various events on the interface (clicks, focus acquisition, etc.) notify the server to callback event functions via SignalR. These callbacks are inserted externally to interact with SignalR. Without special handling, the Scope belonging to the callback event is a newly created Scope for that event. However, the Component we interact with in the callback event is created within the Scope belonging to SignalR, resulting in cross-Scope interaction. PinnedScopeComponentBase addresses this by resetting PinnedScope.Scope to the SignalR corresponding Scope before executing the callback function.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on DependencyInjection.StaticAccessor.Blazor:

Package Downloads
DependencyInjection.StaticAccessor.Blazor.WebAssembly

This is for the Blazor client-side project and provides a setup method to ensure that you can use the static property PinnedScope.ScopedServices.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
9.0.0 103 12/14/2024
9.0.0-preview-1733580617 73 12/7/2024
8.1.0 93 12/13/2024
8.1.0-preview-1734072808 72 12/13/2024
8.0.0 177 9/18/2024
8.0.0-preview-1726694007 96 9/18/2024
8.0.0-preview-1726647274 91 9/18/2024
8.0.0-preview-1726214654 92 9/13/2024
8.0.0-preview-1726080008 88 9/11/2024

# 8.1

1. .NET 7.0 新增`HostApplicationBuilder`,但由于无法从`HostApplicationBuilder`对象获取`IHostBuilder`对象,所以对为`HostApplicationBuilder`新增扩展方法`UsePinnedScopeServiceProvider`和`UseEditableServiceProvider`。
2. 使用`UnsafeAccessorAttribute`替代反射访问`ComponentBase.HandleEventAsync`,提升访问性能
3. 针对 Blazor 项目,为`IHostBuilder`和`HostApplicationBuilder`新增`UseOwningScopedServices`扩展方法。在应用初始化时调用该方法后,对于继承了`PinnedScopeOwningComponentBase`的页面的事件回调方法及后续调用方法,通过`PinnedScope.ScopedServices`获取的`IServiceProvider`等同于`OwningComponentBase.ScopedServices`。

   ```csharp
   // Main
   var builder = WebApplication.CreateBuilder(args);

   // ...

   builder.Host
       .UsePinnedScopeServiceProvider()  // 调用该方法完成 PinnedScope 初始化
       .UseOwningScopedServices();       // 调用该方法后,对于继承 PinnedScopeOwningComponentBase 的页面,在回调方法及后续调用方法中,通过 PinnedScope.ScopedServices 获取到的为 OwningCompoenetBase.Service

   var app = builder.Build();

   // ...

   app.Run();


   // Test.razor
   @page "/test"
   @using DependencyInjection.StaticAccessor
   @using DependencyInjection.StaticAccessor.Blazor
   @rendermode InteractiveServer
   @inject IServiceProvider serviceProvider
   @inherits PinnedScopeOwningComponentBase

   <PageTitle>Test</PageTitle>

   <button class="btn btn-primary" @onclick="Call">Click</button>

   @code {
       private void Call()
       {
           var equals1 = serviceProvider == PinnedScope.ScopedServices;
           var equals2 = ScopedServices == PinnedScope.ScopedServices;

           /**
            * 1. 如果初始化时不调用 UseOwningScopedServices 扩展方法,结果如下
            *     equals1: true, equals2: false
            *
            * 2. 如果初始化时调用 UseOwningScopedServices 扩展方法,结果如下
            *     equals1: false, equals2: true
            */
       }
   }
   ```

4. 为了实现第四项的功能,修改了`PinnedScopeComponentBase`, `PinnedScopeOwningComponentBase`和`PinnedScopeLayoutComponentBase`的实现,如果之前按照 [已有自定义ComponentBase基类的解决方案](https://github.com/inversionhourglass/DependencyInjection.StaticAccessor/blob/master/README.md#%E5%B7%B2%E6%9C%89%E8%87%AA%E5%AE%9A%E4%B9%89componentbase%E5%9F%BA%E7%B1%BB%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88) 修改了`ComponentBase`系列基类,现在你需要重新参照最新的 [已有自定义ComponentBase基类的解决方案](https://github.com/inversionhourglass/DependencyInjection.StaticAccessor/blob/legacy/8.0/README.md#%E5%B7%B2%E6%9C%89%E8%87%AA%E5%AE%9A%E4%B9%89componentbase%E5%9F%BA%E7%B1%BB%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88) 进行修改。