Browse Source

docs: Improved DI documentation (#2407)

tags/3.8.0
Armano den Boef GitHub 2 years ago
parent
commit
6fdcf98240
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 460 additions and 198 deletions
  1. +69
    -0
      docs/guides/dependency_injection/basics.md
  2. BIN
      docs/guides/dependency_injection/images/manager.png
  3. +44
    -0
      docs/guides/dependency_injection/injection.md
  4. +9
    -0
      docs/guides/dependency_injection/samples/access-activator.cs
  5. +13
    -0
      docs/guides/dependency_injection/samples/collection.cs
  6. +14
    -0
      docs/guides/dependency_injection/samples/ctor-injecting.cs
  7. +18
    -0
      docs/guides/dependency_injection/samples/enumeration.cs
  8. +12
    -0
      docs/guides/dependency_injection/samples/implicit-registration.cs
  9. +16
    -0
      docs/guides/dependency_injection/samples/modules.cs
  10. +24
    -0
      docs/guides/dependency_injection/samples/program.cs
  11. +9
    -0
      docs/guides/dependency_injection/samples/property-injecting.cs
  12. +26
    -0
      docs/guides/dependency_injection/samples/provider.cs
  13. +17
    -0
      docs/guides/dependency_injection/samples/runasync.cs
  14. +6
    -0
      docs/guides/dependency_injection/samples/scoped.cs
  15. +21
    -0
      docs/guides/dependency_injection/samples/service-registration.cs
  16. +9
    -0
      docs/guides/dependency_injection/samples/services.cs
  17. +6
    -0
      docs/guides/dependency_injection/samples/singleton.cs
  18. +6
    -0
      docs/guides/dependency_injection/samples/transient.cs
  19. +39
    -0
      docs/guides/dependency_injection/scaling.md
  20. +48
    -0
      docs/guides/dependency_injection/services.md
  21. +52
    -0
      docs/guides/dependency_injection/types.md
  22. +0
    -13
      docs/guides/int_framework/dependency-injection.md
  23. +1
    -2
      docs/guides/int_framework/intro.md
  24. +0
    -51
      docs/guides/text_commands/dependency-injection.md
  25. +1
    -1
      docs/guides/text_commands/intro.md
  26. +0
    -65
      docs/guides/text_commands/samples/dependency-injection/dependency_map_setup.cs
  27. +0
    -37
      docs/guides/text_commands/samples/dependency-injection/dependency_module.cs
  28. +0
    -29
      docs/guides/text_commands/samples/dependency-injection/dependency_module_noinject.cs

+ 69
- 0
docs/guides/dependency_injection/basics.md View File

@@ -0,0 +1,69 @@
---
uid: Guides.DI.Intro
title: Introduction
---

# Dependency Injection

Dependency injection is a feature not required in Discord.Net, but makes it a lot easier to use.
It can be combined with a large number of other libraries, and gives you better control over your application.

> Further into the documentation, Dependency Injection will be referred to as 'DI'.

## Installation

DI is not native to .NET. You need to install the extension packages to your project in order to use it:

- [Meta](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection/).
- [Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection.Abstractions/).

> [!WARNING]
> Downloading the abstractions package alone will not give you access to required classes to use DI properly.
> Please install both packages, or choose to only install the meta package to implicitly install both.

### Visual Package Manager:

[Installing](images/manager.png)

### Command Line:

`PM> Install-Package Microsoft.Extensions.DependencyInjection`.

> [!TIP]
> ASP.NET already comes packed with all the necessary assemblies in its framework.
> You do not require to install any additional NuGet packages to make full use of all features of DI in ASP.NET projects.

## Getting started

First of all, you will need to create an application based around dependency injection,
which in order will be able to access and inject them across the project.

[!code-csharp[Building the Program](samples/program.cs)]

In order to freely pass around your dependencies in different classes,
you will need to register them to a new `ServiceCollection` and build them into an `IServiceProvider` as seen above.
The IServiceProvider then needs to be accessible by the startup file, so you can access your provider and manage them.

[!code-csharp[Building the Collection](samples/collection.cs)]

As shown above, an instance of `DiscordSocketConfig` is created, and added **before** the client itself is.
Because the collection will prefer to create the highest populated constructor available with the services already present,
it will prefer the constructor with the configuration, because you already added it.

## Using your dependencies

After building your provider in the Program class constructor, the provider is now available inside the instance you're actively using.
Through the provider, we can ask for the DiscordSocketClient we registered earlier.

[!code-csharp[Applying DI in RunAsync](samples/runasync.cs)]

> [!WARNING]
> Service constructors are not activated until the service is **first requested**.
> An 'endpoint' service will have to be requested from the provider before it is activated.
> If a service is requested with dependencies, its dependencies (if not already active) will be activated before the service itself is.

## Injecting dependencies

You can not only directly access the provider from a field or property, but you can also pass around instances to classes registered in the provider.
There are multiple ways to do this. Please refer to the
[Injection Documentation](Guides.DI.Injection) for further information.

BIN
docs/guides/dependency_injection/images/manager.png View File

Before After
Width: 777  |  Height: 142  |  Size: 12 KiB

+ 44
- 0
docs/guides/dependency_injection/injection.md View File

@@ -0,0 +1,44 @@
---
uid: Guides.DI.Injection
title: Injection
---

# Injecting instances within the provider

You can inject registered services into any class that is registered to the `IServiceProvider`.
This can be done through property or constructor.

> [!NOTE]
> As mentioned above, the dependency *and* the target class have to be registered in order for the serviceprovider to resolve it.

## Injecting through a constructor

Services can be injected from the constructor of the class.
This is the preferred approach, because it automatically locks the readonly field in place with the provided service and isn't accessible outside of the class.

[!code-csharp[Property Injection(samples/property-injecting.cs)]]

## Injecting through properties

Injecting through properties is also allowed as follows.

[!code-csharp[Property Injection](samples/property-injecting.cs)]

> [!WARNING]
> Dependency Injection will not resolve missing services in property injection, and it will not pick a constructor instead.
> If a publically accessible property is attempted to be injected and its service is missing, the application will throw an error.

## Using the provider itself

You can also access the provider reference itself from injecting it into a class. There are multiple use cases for this:

- Allowing libraries (Like Discord.Net) to access your provider internally.
- Injecting optional dependencies.
- Calling methods on the provider itself if necessary, this is often done for creating scopes.

[!code-csharp[Provider Injection](samples/provider.cs)]

> [!NOTE]
> It is important to keep in mind that the provider will pick the 'biggest' available constructor.
> If you choose to introduce multiple constructors,
> keep in mind that services missing from one constructor may have the provider pick another one that *is* available instead of throwing an exception.

+ 9
- 0
docs/guides/dependency_injection/samples/access-activator.cs View File

@@ -0,0 +1,9 @@
async Task RunAsync()
{
//...

await _serviceProvider.GetRequiredService<ServiceActivator>()
.ActivateAsync();

//...
}

+ 13
- 0
docs/guides/dependency_injection/samples/collection.cs View File

@@ -0,0 +1,13 @@
static IServiceProvider CreateServices()
{
var config = new DiscordSocketConfig()
{
//...
};

var collection = new ServiceCollection()
.AddSingleton(config)
.AddSingleton<DiscordSocketClient>();

return collection.BuildServiceProvider();
}

+ 14
- 0
docs/guides/dependency_injection/samples/ctor-injecting.cs View File

@@ -0,0 +1,14 @@
public class ClientHandler
{
private readonly DiscordSocketClient _client;

public ClientHandler(DiscordSocketClient client)
{
_client = client;
}

public async Task ConfigureAsync()
{
//...
}
}

+ 18
- 0
docs/guides/dependency_injection/samples/enumeration.cs View File

@@ -0,0 +1,18 @@
public class ServiceActivator
{
// This contains *all* registered services of serviceType IService
private readonly IEnumerable<IService> _services;

public ServiceActivator(IEnumerable<IService> services)
{
_services = services;
}

public async Task ActivateAsync()
{
foreach(var service in _services)
{
await service.StartAsync();
}
}
}

+ 12
- 0
docs/guides/dependency_injection/samples/implicit-registration.cs View File

@@ -0,0 +1,12 @@
public static ServiceCollection RegisterImplicitServices(this ServiceCollection collection, Type interfaceType, Type activatorType)
{
// Get all types in the executing assembly. There are many ways to do this, but this is fastest.
foreach (var type in typeof(Program).Assembly.GetTypes())
{
if (interfaceType.IsAssignableFrom(type) && !type.IsAbstract)
collection.AddSingleton(interfaceType, type);
}

// Register the activator so you can activate the instances.
collection.AddSingleton(activatorType);
}

+ 16
- 0
docs/guides/dependency_injection/samples/modules.cs View File

@@ -0,0 +1,16 @@
public class MyModule : InteractionModuleBase
{
private readonly MyService _service;

public MyModule(MyService service)
{
_service = service;
}

[SlashCommand("things", "Shows things")]
public async Task ThingsAsync()
{
var str = string.Join("\n", _service.Things)
await RespondAsync(str);
}
}

+ 24
- 0
docs/guides/dependency_injection/samples/program.cs View File

@@ -0,0 +1,24 @@
public class Program
{
private readonly IServiceProvider _serviceProvider;

public Program()
{
_serviceProvider = CreateProvider();
}

static void Main(string[] args)
=> new Program().RunAsync(args).GetAwaiter().GetResult();

static IServiceProvider CreateProvider()
{
var collection = new ServiceCollection();
//...
return collection.BuildServiceProvider();
}

async Task RunAsync(string[] args)
{
//...
}
}

+ 9
- 0
docs/guides/dependency_injection/samples/property-injecting.cs View File

@@ -0,0 +1,9 @@
public class ClientHandler
{
public DiscordSocketClient Client { get; set; }

public async Task ConfigureAsync()
{
//...
}
}

+ 26
- 0
docs/guides/dependency_injection/samples/provider.cs View File

@@ -0,0 +1,26 @@
public class UtilizingProvider
{
private readonly IServiceProvider _provider;
private readonly AnyService _service;

// This service is allowed to be null because it is only populated if the service is actually available in the provider.
private readonly AnyOtherService? _otherService;

// This constructor injects only the service provider,
// and uses it to populate the other dependencies.
public UtilizingProvider(IServiceProvider provider)
{
_provider = provider;
_service = provider.GetRequiredService<AnyService>();
_otherService = provider.GetService<AnyOtherService>();
}

// This constructor injects the service provider, and AnyService,
// making sure that AnyService is not null without having to call GetRequiredService
public UtilizingProvider(IServiceProvider provider, AnyService service)
{
_provider = provider;
_service = service;
_otherService = provider.GetService<AnyOtherService>();
}
}

+ 17
- 0
docs/guides/dependency_injection/samples/runasync.cs View File

@@ -0,0 +1,17 @@
async Task RunAsync(string[] args)
{
// Request the instance from the client.
// Because we're requesting it here first, its targetted constructor will be called and we will receive an active instance.
var client = _services.GetRequiredService<DiscordSocketClient>();

client.Log += async (msg) =>
{
await Task.CompletedTask;
Console.WriteLine(msg);
}

await client.LoginAsync(TokenType.Bot, "");
await client.StartAsync();

await Task.Delay(Timeout.Infinite);
}

+ 6
- 0
docs/guides/dependency_injection/samples/scoped.cs View File

@@ -0,0 +1,6 @@

// With serviceType:
collection.AddScoped<IScopedService, ScopedService>();

// Without serviceType:
collection.AddScoped<ScopedService>();

+ 21
- 0
docs/guides/dependency_injection/samples/service-registration.cs View File

@@ -0,0 +1,21 @@
static IServiceProvider CreateServices()
{
var config = new DiscordSocketConfig()
{
//...
};

// X represents either Interaction or Command, as it functions the exact same for both types.
var servConfig = new XServiceConfig()
{
//...
}

var collection = new ServiceCollection()
.AddSingleton(config)
.AddSingleton<DiscordSocketClient>()
.AddSingleton(servConfig)
.AddSingleton<XService>();

return collection.BuildServiceProvider();
}

+ 9
- 0
docs/guides/dependency_injection/samples/services.cs View File

@@ -0,0 +1,9 @@
public class MyService
{
public List<string> Things { get; }

public MyService()
{
Things = new();
}
}

+ 6
- 0
docs/guides/dependency_injection/samples/singleton.cs View File

@@ -0,0 +1,6 @@

// With serviceType:
collection.AddSingleton<ISingletonService, SingletonService>();

// Without serviceType:
collection.AddSingleton<SingletonService>();

+ 6
- 0
docs/guides/dependency_injection/samples/transient.cs View File

@@ -0,0 +1,6 @@

// With serviceType:
collection.AddTransient<ITransientService, TransientService>();

// Without serviceType:
collection.AddTransient<TransientService>();

+ 39
- 0
docs/guides/dependency_injection/scaling.md View File

@@ -0,0 +1,39 @@
---
uid: Guides.DI.Scaling
title: Scaling your DI
---

# Scaling your DI

Dependency injection has a lot of use cases, and is very suitable for scaled applications.
There are a few ways to make registering & using services easier in large amounts.

## Using a range of services.

If you have a lot of services that all have the same use such as handling an event or serving a module,
you can register and inject them all at once by some requirements:

- All classes need to inherit a single interface or abstract type.
- While not required, it is preferred if the interface and types share a method to call on request.
- You need to register a class that all the types can be injected into.

### Registering implicitly

Registering all the types is done through getting all types in the assembly and checking if they inherit the target interface.

[!code-csharp[Registering](samples/implicit-registration.cs)]

> [!NOTE]
> As seen above, the interfaceType and activatorType are undefined. For our usecase below, these are `IService` and `ServiceActivator` in order.

### Using implicit dependencies

In order to use the implicit dependencies, you have to get access to the activator you registered earlier.

[!code-csharp[Accessing the activator](samples/access-activator.cs)]

When the activator is accessed and the `ActivateAsync()` method is called, the following code will be executed:

[!code-csharp[Executing the activator](samples/enumeration.cs)]

As a result of this, all the services that were registered with `IService` as its implementation type will execute their starting code, and start up.

+ 48
- 0
docs/guides/dependency_injection/services.md View File

@@ -0,0 +1,48 @@
---
uid: Guides.DI.Services
title: Using DI in Interaction & Command Frameworks
---

# DI in the Interaction- & Command Service

For both the Interaction- and Command Service modules, DI is quite straight-forward to use.

You can inject any service into modules without the modules having to be registered to the provider.
Discord.Net resolves your dependencies internally.

> [!WARNING]
> The way DI is used in the Interaction- & Command Service are nearly identical, except for one detail:
> [Resolving Module Dependencies](xref:Guides.IntFw.Intro#resolving-module-dependencies)

## Registering the Service

Thanks to earlier described behavior of allowing already registered members as parameters of the available ctors,
The socket client & configuration will automatically be acknowledged and the XService(client, config) overload will be used.

[!code-csharp[Service Registration](samples/service-registration.cs)]

## Usage in modules

In the constructor of your module, any parameters will be filled in by
the @System.IServiceProvider that you've passed.

Any publicly settable properties will also be filled in the same
manner.

[!code-csharp[Module Injection](samples/modules.cs)]

If you accept `Command/InteractionService` or `IServiceProvider` as a parameter in your constructor or as an injectable property,
these entries will be filled by the `Command/InteractionService` that the module is loaded from and the `IServiceProvider` that is passed into it respectively.

> [!NOTE]
> Annotating a property with a [DontInjectAttribute] attribute will
> prevent the property from being injected.

## Services

Because modules are transient of nature and will reinstantiate on every request,
it is suggested to create a singleton service behind it to hold values across multiple command executions.

[!code-csharp[Services](samples/services.cs)]



+ 52
- 0
docs/guides/dependency_injection/types.md View File

@@ -0,0 +1,52 @@
---
uid: Guides.DI.Dependencies
title: Types of Dependencies
---

# Dependency Types

There are 3 types of dependencies to learn to use. Several different usecases apply for each.

> [!WARNING]
> When registering types with a serviceType & implementationType,
> only the serviceType will be available for injection, and the implementationType will be used for the underlying instance.

## Singleton

A singleton service creates a single instance when first requested, and maintains that instance across the lifetime of the application.
Any values that are changed within a singleton will be changed across all instances that depend on it, as they all have the same reference to it.

### Registration:

[!code-csharp[Singleton Example](samples/singleton.cs)]

> [!NOTE]
> Types like the Discord client and Interaction/Command services are intended to be singleton,
> as they should last across the entire app and share their state with all references to the object.

## Scoped

A scoped service creates a new instance every time a new service is requested, but is kept across the 'scope'.
As long as the service is in view for the created scope, the same instance is used for all references to the type.
This means that you can reuse the same instance during execution, and keep the services' state for as long as the request is active.

### Registration:

[!code-csharp[Scoped Example](samples/scoped.cs)]

> [!NOTE]
> Without using HTTP or libraries like EFCORE, scopes are often unused in Discord bots.
> They are most commonly used for handling HTTP and database requests.

## Transient

A transient service is created every time it is requested, and does not share its state between references within the target service.
It is intended for lightweight types that require little state, to be disposed quickly after execution.

### Registration:

[!code-csharp[Transient Example](samples/transient.cs)]

> [!NOTE]
> Discord.Net modules behave exactly as transient types, and are intended to only last as long as the command execution takes.
> This is why it is suggested for apps to use singleton services to keep track of cross-execution data.

+ 0
- 13
docs/guides/int_framework/dependency-injection.md View File

@@ -1,13 +0,0 @@
---
uid: Guides.IntFw.DI
title: Dependency Injection
---

# Dependency Injection

Dependency injection in the Interaction Service is mostly based on that of the Text-based command service,
for which further information is found [here](xref:Guides.TextCommands.DI).

> [!NOTE]
> The 2 are nearly identical, except for one detail:
> [Resolving Module Dependencies](xref:Guides.IntFw.Intro#resolving-module-dependencies)

+ 1
- 2
docs/guides/int_framework/intro.md View File

@@ -374,8 +374,7 @@ delegate can be used to create HTTP responses from a deserialized json object st
- Use the interaction endpoints of the module base instead of the interaction object (ie. `RespondAsync()`, `FollowupAsync()`...).

[AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion
[DependencyInjection]: xref:Guides.TextCommands.DI
[Post Execution Docuemntation]: xref:Guides.IntFw.PostExecution
[DependencyInjection]: xref:Guides.DI.Intro

[GroupAttribute]: xref:Discord.Interactions.GroupAttribute
[InteractionService]: xref:Discord.Interactions.InteractionService


+ 0
- 51
docs/guides/text_commands/dependency-injection.md View File

@@ -1,51 +0,0 @@
---
uid: Guides.TextCommands.DI
title: Dependency Injection
---

# Dependency Injection

The Text Command Service is bundled with a very barebone Dependency
Injection service for your convenience. It is recommended that you use
DI when writing your modules.

> [!WARNING]
> If you were brought here from the Interaction Service guides,
> make sure to replace all namespaces that imply `Discord.Commands` with `Discord.Interactions`

## Setup

1. Create a @Microsoft.Extensions.DependencyInjection.ServiceCollection.
2. Add the dependencies to the service collection that you wish
to use in the modules.
3. Build the service collection into a service provider.
4. Pass the service collection into @Discord.Commands.CommandService.AddModulesAsync* / @Discord.Commands.CommandService.AddModuleAsync* , @Discord.Commands.CommandService.ExecuteAsync* .

### Example - Setting up Injection

[!code-csharp[IServiceProvider Setup](samples/dependency-injection/dependency_map_setup.cs)]

## Usage in Modules

In the constructor of your module, any parameters will be filled in by
the @System.IServiceProvider that you've passed.

Any publicly settable properties will also be filled in the same
manner.

> [!NOTE]
> Annotating a property with a [DontInjectAttribute] attribute will
> prevent the property from being injected.

> [!NOTE]
> If you accept `CommandService` or `IServiceProvider` as a parameter
> in your constructor or as an injectable property, these entries will
> be filled by the `CommandService` that the module is loaded from and
> the `IServiceProvider` that is passed into it respectively.

### Example - Injection in Modules

[!code-csharp[Injection Modules](samples/dependency-injection/dependency_module.cs)]
[!code-csharp[Disallow Dependency Injection](samples/dependency-injection/dependency_module_noinject.cs)]

[DontInjectAttribute]: xref:Discord.Commands.DontInjectAttribute

+ 1
- 1
docs/guides/text_commands/intro.md View File

@@ -187,7 +187,7 @@ service provider.

### Module Constructors

Modules are constructed using [Dependency Injection](xref:Guides.TextCommands.DI). Any parameters
Modules are constructed using [Dependency Injection](xref:Guides.DI.Intro). Any parameters
that are placed in the Module's constructor must be injected into an
@System.IServiceProvider first.



+ 0
- 65
docs/guides/text_commands/samples/dependency-injection/dependency_map_setup.cs View File

@@ -1,65 +0,0 @@
public class Initialize
{
private readonly CommandService _commands;
private readonly DiscordSocketClient _client;

// Ask if there are existing CommandService and DiscordSocketClient
// instance. If there are, we retrieve them and add them to the
// DI container; if not, we create our own.
public Initialize(CommandService commands = null, DiscordSocketClient client = null)
{
_commands = commands ?? new CommandService();
_client = client ?? new DiscordSocketClient();
}

public IServiceProvider BuildServiceProvider() => new ServiceCollection()
.AddSingleton(_client)
.AddSingleton(_commands)
// You can pass in an instance of the desired type
.AddSingleton(new NotificationService())
// ...or by using the generic method.
//
// The benefit of using the generic method is that
// ASP.NET DI will attempt to inject the required
// dependencies that are specified under the constructor
// for us.
.AddSingleton<DatabaseService>()
.AddSingleton<CommandHandler>()
.BuildServiceProvider();
}
public class CommandHandler
{
private readonly DiscordSocketClient _client;
private readonly CommandService _commands;
private readonly IServiceProvider _services;

public CommandHandler(IServiceProvider services, CommandService commands, DiscordSocketClient client)
{
_commands = commands;
_services = services;
_client = client;
}

public async Task InitializeAsync()
{
// Pass the service provider to the second parameter of
// AddModulesAsync to inject dependencies to all modules
// that may require them.
await _commands.AddModulesAsync(
assembly: Assembly.GetEntryAssembly(),
services: _services);
_client.MessageReceived += HandleCommandAsync;
}

public async Task HandleCommandAsync(SocketMessage msg)
{
// ...
// Pass the service provider to the ExecuteAsync method for
// precondition checks.
await _commands.ExecuteAsync(
context: context,
argPos: argPos,
services: _services);
// ...
}
}

+ 0
- 37
docs/guides/text_commands/samples/dependency-injection/dependency_module.cs View File

@@ -1,37 +0,0 @@
// After setting up dependency injection, modules will need to request
// the dependencies to let the library know to pass
// them along during execution.

// Dependency can be injected in two ways with Discord.Net.
// You may inject any required dependencies via...
// the module constructor
// -or-
// public settable properties

// Injection via constructor
public class DatabaseModule : ModuleBase<SocketCommandContext>
{
private readonly DatabaseService _database;
public DatabaseModule(DatabaseService database)
{
_database = database;
}

[Command("read")]
public async Task ReadFromDbAsync()
{
await ReplyAsync(_database.GetData());
}
}

// Injection via public settable properties
public class DatabaseModule : ModuleBase<SocketCommandContext>
{
public DatabaseService DbService { get; set; }

[Command("read")]
public async Task ReadFromDbAsync()
{
await ReplyAsync(DbService.GetData());
}
}

+ 0
- 29
docs/guides/text_commands/samples/dependency-injection/dependency_module_noinject.cs View File

@@ -1,29 +0,0 @@
// Sometimes injecting dependencies automatically with the provided
// methods in the prior example may not be desired.

// You may explicitly tell Discord.Net to **not** inject the properties
// by either...
// restricting the access modifier
// -or-
// applying DontInjectAttribute to the property

// Restricting the access modifier of the property
public class ImageModule : ModuleBase<SocketCommandContext>
{
public ImageService ImageService { get; }
public ImageModule()
{
ImageService = new ImageService();
}
}

// Applying DontInjectAttribute
public class ImageModule : ModuleBase<SocketCommandContext>
{
[DontInject]
public ImageService ImageService { get; set; }
public ImageModule()
{
ImageService = new ImageService();
}
}

Loading…
Cancel
Save