-
Notifications
You must be signed in to change notification settings - Fork 125
Add chapter about dependency injection in Blazor #14
Changes from 4 commits
5329939
1af0fc4
1f893e5
87e3898
451c393
4f3198e
ba62fb7
b6bdd08
4163a75
d8431e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
--- | ||
title: Dependency injection in Blazor | ||
author: rstropek | ||
description: Learn how Blazor apps can use built-in services by having them injected into components. | ||
manager: wpickett | ||
ms.author: riande | ||
ms.custom: mvc | ||
ms.date: 04/09/2018 | ||
ms.prod: asp.net-core | ||
ms.technology: aspnet | ||
ms.topic: article | ||
uid: client-side/blazor/dependency-injection | ||
--- | ||
# Dependency injection in Blazor | ||
|
||
By [Rainer Stropek](https://www.timecockpit.com) | ||
|
||
[!INCLUDE[](~/includes/blazor-preview-notice.md)] | ||
|
||
Blazor has [dependency injection (DI)](https://docs.microsoft.com/aspnet/core/fundamentals/dependency-injection) built-in. Apps can use built-in services by having them injected into components. Apps can also define custom services and make them available via DI. | ||
|
||
## What is dependency injection? | ||
|
||
DI is a technique for accessing services configured in a central location. This can be useful to: | ||
|
||
* Share a single instance of a service class across many components (known as a *singleton* service). | ||
* Decouple components from particular concrete service classes and only reference abstractions. For example, an interface `IDataAccess` is implemented by a concrete class `DataAccess`. When a component uses DI to receive an `IDataAccess` implementation, the component isn't coupled to the concrete type. The implementation can be swapped, perhaps to a mock implementation in unit tests. | ||
|
||
Blazor's DI system is responsible for supplying instances of services to components. DI also resolves dependencies recursively so that services themselves can depend on further services. DI is configured during startup of the app. An example is shown later in this topic. | ||
|
||
## Use of existing .NET mechanisms | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this section adds much and it delays getting into content that helps you get started using DI. I think I would just remove it. |
||
|
||
DI in Blazor is based on .NET's [System.IServiceProvider](https://docs.microsoft.com/dotnet/api/system.iserviceprovider) interface. The interface defines a generic mechanism for retrieving a service object in .NET apps. | ||
|
||
Blazor's implementation of `System.IServiceProvider` obtains its services from an underlying [Microsoft.Extensions.DependencyInjection.IServiceCollection](https://docs.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection). | ||
|
||
## Add services to DI | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool - good explanation about configuration! |
||
|
||
After creating a new app, examine the `Main` method in *Program.cs*: | ||
|
||
```csharp | ||
static void Main(string[] args) | ||
{ | ||
var serviceProvider = new BrowserServiceProvider(configure => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One more thing: I'm proposing that rename There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
{ | ||
// Add any custom services here | ||
}); | ||
|
||
new BrowserRenderer(serviceProvider).AddComponent<App>("app"); | ||
} | ||
``` | ||
|
||
`BrowserServiceProvider` receives an action with which you can add your app services to DI. `configure` references the underlying `IServiceCollection`, which is a list of service descriptor objects ([Microsoft.Extensions.DependencyInjection.ServiceDescriptor](https://docs.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.servicedescriptor)). Services are added by providing service descriptors to the service collection. The following is a code sample demonstrating the concept: | ||
|
||
```csharp | ||
static void Main(string[] args) | ||
{ | ||
var serviceProvider = new BrowserServiceProvider(configure => | ||
{ | ||
configure.Add(ServiceDescriptor.Singleton<IDataAccess, DataAccess>()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should just be |
||
}); | ||
|
||
new BrowserRenderer(serviceProvider).AddComponent<App>("app"); | ||
} | ||
``` | ||
|
||
`ServiceDescriptor` offers several overloads of three methods that are used to add services to Blazor's DI: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think of |
||
|
||
| Method | Description | | ||
| ----------- | ----------- | | ||
| `Singleton` | DI creates a *single instance* of the service. All components requiring this service receive a reference to this instance. | | ||
| `Transient` | Whenever a component requires this service, it receives a *new instance* of the service. | | ||
| `Scoped` | Blazor doesn't currently have the concept of DI scopes. `Scoped` behaves like `Singleton`. Therefore, prefer `Singleton` and avoid `Scoped`. | | ||
|
||
## Default services | ||
|
||
Blazor provides default services that are automatically added to the service collection of an app. The following table shows a list of the default services currently provided by Blazor's `BrowserServiceProvider`. | ||
|
||
| Method | Description | | ||
| ------------ | ----------- | | ||
| `IUriHelper` | Helpers for working with URIs and navigation state (singleton). | | ||
| `HttpClient` | Provides methods for sending HTTP requests and receiving HTTP responses from a resource identified by a URI (singleton). Note that this instance of [System.Net.Http.HttpClient](https://docs.microsoft.com/dotnet/api/system.net.http.httpclient) uses the browser for handling the HTTP traffic in the background. Its [BaseAddress](https://docs.microsoft.com/dotnet/api/system.net.http.httpclient.baseaddress) is automatically set to the base URI prefix of the app. | | ||
|
||
Note that it is possible to use a custom services provider instead of the default `BrowserServiceProvider` that's added by the default template. A custom service provider doesn't automatically provide the default services listed in the table. Those services must be added to the new service provider explicitly. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @SteveSandersonMS Is this a bug? Shouldn't we populate the custom service provider with the default services? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That’s not something we have the option to do. If the developer provides their own IServiceProvider we can’t control what it returns; it’s up to them to delegate calls to a BrowserServiceProvider if they want to. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That seems surprisingly different to how things work in ASP.NET Core. In ASP.NET Core you can switch to using a different IoC container (Autofac or whatever) but you still get the host provided services. But no need to hold up this PR on this discussion as the doc is currently correct. |
||
|
||
## Request a service in a component | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this section - well explained! |
||
|
||
Once services are added to the service collection, they can be injected into the components' Razor templates using the `@inject` Razor directive. `@inject` has two parameters: | ||
|
||
* Type name: The type of the service to inject. | ||
* Property name: The name of the property receiving the injected app service. Note that the property doesn't require manual creation. The compiler creates the property. | ||
|
||
Multiple `@inject` statements can be used to inject different services. | ||
|
||
The following example shows how to use `@inject`. The service implementing `Services.IDataAccess` is injected into the component's property `DataRepository`. Note how the code is only using the `IDataAccess` abstraction: | ||
|
||
```csharp | ||
@page "/customer-list" | ||
@using Services | ||
@inject IDataAccess DataRepository | ||
|
||
<ul> | ||
@if (Customers != null) | ||
{ | ||
@foreach (var customer in Customers) | ||
{ | ||
<li>@customer.FirstName @customer.LastName</li> | ||
} | ||
} | ||
</ul> | ||
|
||
@functions { | ||
private IReadOnlyList<Customer> Customers; | ||
|
||
protected override async Task OnInitAsync() | ||
{ | ||
// The property DataRepository has received an implemenation | ||
// of IDataAccess through dependency injection. We can use | ||
// it to get data from our server. | ||
Customers = await DataRepository.GetAllCustomersAsync(); | ||
} | ||
} | ||
``` | ||
|
||
Internally, the generated property (`DataRepository`) is decorated with the `Microsoft.AspNetCore.Blazor.Components.InjectAttribute` attribute. Typically, this attribute isn't used directly. If a base class is required for components and injected properties are also required for the base class, `InjectAttribute` can be manually added: | ||
|
||
```csharp | ||
public class ComponentBase : BlazorComponent | ||
{ | ||
// Note that Blazor's dependency injection works even if using the | ||
// InjectAttribute in a component's base class. | ||
[Inject] | ||
protected IDataAccess DataRepository { get; set; } | ||
... | ||
} | ||
``` | ||
|
||
In components derived from the base class, the `@inject` directive isn't required. The `InjectAttribute` of the base class is satisfactory: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. satisfactory -> sufficient |
||
|
||
```csharp | ||
@page "/demo" | ||
@inherits ComponentBase | ||
|
||
<h1>...</h1> | ||
... | ||
``` | ||
|
||
## Dependency injection in services | ||
|
||
Complex services might require additional services. In the prior example, `DataAccess` might require Blazor's default service `HttpClient`. `@inject` or the `InjectAttribute` can't be used in services. *Constructor injection* must be used instead. Required services are added by adding parameters to the service's constructor. When dependency injection creates the service, it recognizes the services it requires in the constructor and provides them accordingly. | ||
|
||
The following code sample demonstrates the concept: | ||
|
||
```csharp | ||
public class DataAccess : IDataAccess | ||
{ | ||
// Note that the constructor receives an HttpClient via dependency | ||
// injection. HttpClient is a default service offered by Blazor. | ||
public Repository(HttpClient client) | ||
{ | ||
... | ||
} | ||
... | ||
} | ||
``` | ||
|
||
Note the following prerequisites for constructor injection: | ||
|
||
* There must be one constructor whose arguments can all be fulfilled by dependency injection. Note that additional parameters not covered by DI are allowed if default values are specified for them. | ||
* The applicable constructor must be *public*. | ||
* There must only be one applicable constructor. In case of an ambiguity, DI throws an exception. | ||
|
||
## Service lifetime | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Services are created by the service provider, so they need to be disposed by the service provider. We need to address this as part of https://github.com/aspnet/Blazor/issues/464, but regardless I think this section should be removed as it contradicts that guidance. The content about implementing |
||
|
||
Note that Blazor doesn't automatically dispose injected services that implement `IDisposable`. Components can implement `IDisposable`. Services are disposed when the user navigates away from the component. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think this should say: "Components are disposed when the user navigates away from the component, at which point the component can dispose any transient services" |
||
|
||
The following code sample demonstrates how to implement `IDisposable` in a component: | ||
|
||
```csharp | ||
... | ||
@using System; | ||
@implements IDisposable | ||
... | ||
|
||
@functions { | ||
... | ||
public void Dispose() | ||
{ | ||
// Add code for disposing here | ||
... | ||
} | ||
} | ||
``` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need a link to https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection somewhere. Maybe just have an additional resources section at the end? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Firstly, I really like the overall tone of voice here and think this is just the right amount of background detail. Great job!
Secondly, I slightly object to the idea that "component should not create an instance of
DataAccess
" as if that were always true. That's entirely the choice of the application developer. Sometimes there's value in working with an abstraction; sometimes it would just be unnecessary friction. It depends on the scenario. The DI system in Blazor is equally happy to supply instances of concrete service classes as it is to deal with interfaces.I'd say something like:
DI is a technique for accessing services configured in a central location. This can be useful if:
IDataAccess
implemented by a concrete classDataAccess
. If your components use DI to receive anIDataAccess
implementation, they aren't coupled to any concrete type. That means the implementation could easily be swapped, perhaps to a mock implementation in unit tests.Blazor's DI system is responsible for supplying instances of services to components. It also resolves dependencies recursively, so that services themselves can depend on further services, and so on. DI is configured during startup of the app. An example is shown later in this topic.