Skip to content

Commit

Permalink
Improve DI guide (#43)
Browse files Browse the repository at this point in the history
* Improve DI guide

* Lower `injection`

* Add scopes guide

* Use IApplicationCommandResultHandler as an example

* Add a note about the behavior of modules and autocomplete providers
  • Loading branch information
KubaZ2 authored Sep 12, 2024
1 parent 4fd0df7 commit 06c9f0a
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

namespace MyBot;

public class ExampleAutocompleteProvider(string[] data) : IAutocompleteProvider<AutocompleteInteractionContext>
public class DataAutocompleteProvider(IDataProvider dataProvider) : IAutocompleteProvider<AutocompleteInteractionContext>
{
public ValueTask<IEnumerable<ApplicationCommandOptionChoiceProperties>?> GetChoicesAsync(ApplicationCommandInteractionDataOption option, AutocompleteInteractionContext context)
public ValueTask<IEnumerable<ApplicationCommandOptionChoiceProperties>?> GetChoicesAsync(
ApplicationCommandInteractionDataOption option,
AutocompleteInteractionContext context)
{
var input = option.Value!;
var data = dataProvider.GetData();

var result = data.Where(d => d.Contains(input))
.Take(25)
.Select(d => new ApplicationCommandOptionChoiceProperties(d, d));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using NetCord.Services.Commands;

namespace MyBot;

public class DataModule(IDataProvider dataProvider) : CommandModule<CommandContext>
{
[Command("data")]
public string Data(int count) => string.Join(' ', dataProvider.GetData().Take(count));
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
Expand All @@ -10,8 +11,12 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\NetCord.Services\NetCord.Services.csproj" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\NetCord\NetCord.csproj" />
<ProjectReference Include="..\..\..\..\Hosting\NetCord.Hosting.Services\NetCord.Hosting.Services.csproj" />
</ItemGroup>

</Project>

This file was deleted.

11 changes: 11 additions & 0 deletions Documentation/guides/services/DependencyInjection/IDataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace MyBot;

public interface IDataProvider
{
public IReadOnlyList<string> GetData();
}

public class DataProvider : IDataProvider
{
public IReadOnlyList<string> GetData() => ["hello", "world"];
}
35 changes: 35 additions & 0 deletions Documentation/guides/services/DependencyInjection/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using MyBot;

using NetCord;
using NetCord.Gateway;
using NetCord.Hosting.Gateway;
using NetCord.Hosting.Services;
using NetCord.Hosting.Services.ApplicationCommands;
using NetCord.Hosting.Services.Commands;
using NetCord.Services.ApplicationCommands;
using NetCord.Services.Commands;

var builder = Host.CreateApplicationBuilder(args);

builder.Services
.AddSingleton<IDataProvider, DataProvider>()
.AddDiscordGateway(o => o.Configuration = new() { Intents = GatewayIntents.GuildMessages | GatewayIntents.DirectMessages | GatewayIntents.MessageContent })
.AddCommands<CommandContext>()
.AddApplicationCommands<SlashCommandInteraction, SlashCommandContext>();

var host = builder.Build();

host.AddModules(typeof(Program).Assembly);

host.AddSlashCommand<SlashCommandContext>(
name: "data",
description: "Shows the data!",
(IDataProvider dataProvider, SlashCommandContext context, int count) => string.Join(' ', dataProvider.GetData()
.Take(count)));

host.UseGatewayEventHandlers();

await host.RunAsync();
33 changes: 28 additions & 5 deletions Documentation/guides/services/dependency-injection.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
# Dependency Injection

To use dependency injection, simply create a constructor with parameters in a module or an autocomplete provider and then pass `IServiceProvider` as the last parameter of `ExecuteAsync` or `ExecuteAutocompleteAsync` method. It is done automatically when using hosting.
Dependency injection (DI) is a technique that helps make your code more modular and testable by letting you pass services from the outside. It reduces tight coupling between components, making your applications easier to maintain and extend.

## Example Module
[!code-cs[ExampleModule.cs](DependencyInjection/ExampleModule.cs)]
## Scopes

## Example Autocomplete Provider
[!code-cs[ExampleAutocompleteProvider.cs](DependencyInjection/ExampleAutocompleteProvider.cs)]
With `NetCord.Hosting.Services` scopes are created for each command/interaction and disposed after the command/interaction is **completely** executed by default. Therefore all code relevant to the command/interaction like for example the @NetCord.Hosting.Services.ApplicationCommands.IApplicationCommandResultHandler`1 will be executed within the same scope.

You can control whether to use scopes or not by setting the `UseScopes` property in the options class. For example @NetCord.Hosting.Services.ApplicationCommands.ApplicationCommandServiceOptions`2.UseScopes for application commands.

## Minimal APIs

Dependency injection with minimal APIs can seem complicated at first, but it is actually quite simple.

What you need to know is that parameters preceding the context parameter are treated as services and parameters following the context parameter are treated as command/interaction parameters. When the context parameter is not present, all parameters are treated as command/interaction parameters.

You can see an example slash command below, but the same rules apply to all services:
[!code-cs[Program.cs](DependencyInjection/Program.cs#L27-L31)]

## Modules

Dependency injection with modules is like everywhere else. You just inject the services via the constructor. The modules behave as if they were transient services, so they are created for each command/interaction.

You can see an example with text commands below, but the same rules apply to all services:
[!code-cs[DataModule.cs](DependencyInjection/DataModule.cs#l5-L9)]

## Autocomplete Providers

Same for autocomplete providers, you just inject the services via the constructor. They also behave as if they were transient services.

You can see an example autocomplete provider below:
[!code-cs[DataAutocompleteProvider.cs](DependencyInjection/DataAutocompleteProvider.cs#l7-L22)]

0 comments on commit 06c9f0a

Please sign in to comment.