From 06c9f0a96381a8f09c6742649e036acf8b0ffd08 Mon Sep 17 00:00:00 2001 From: Kuba_Z2 <77853483+KubaZ2@users.noreply.github.com> Date: Thu, 12 Sep 2024 18:40:36 +0200 Subject: [PATCH] Improve DI guide (#43) * Improve DI guide * Lower `injection` * Add scopes guide * Use IApplicationCommandResultHandler as an example * Add a note about the behavior of modules and autocomplete providers --- ...rovider.cs => DataAutocompleteProvider.cs} | 8 +++-- .../DependencyInjection/DataModule.cs | 9 +++++ .../DependencyInjection.csproj | 7 +++- .../DependencyInjection/ExampleModule.cs | 9 ----- .../DependencyInjection/IDataProvider.cs | 11 ++++++ .../services/DependencyInjection/Program.cs | 35 +++++++++++++++++++ .../guides/services/dependency-injection.md | 33 ++++++++++++++--- 7 files changed, 95 insertions(+), 17 deletions(-) rename Documentation/guides/services/DependencyInjection/{ExampleAutocompleteProvider.cs => DataAutocompleteProvider.cs} (58%) create mode 100644 Documentation/guides/services/DependencyInjection/DataModule.cs delete mode 100644 Documentation/guides/services/DependencyInjection/ExampleModule.cs create mode 100644 Documentation/guides/services/DependencyInjection/IDataProvider.cs create mode 100644 Documentation/guides/services/DependencyInjection/Program.cs diff --git a/Documentation/guides/services/DependencyInjection/ExampleAutocompleteProvider.cs b/Documentation/guides/services/DependencyInjection/DataAutocompleteProvider.cs similarity index 58% rename from Documentation/guides/services/DependencyInjection/ExampleAutocompleteProvider.cs rename to Documentation/guides/services/DependencyInjection/DataAutocompleteProvider.cs index d8a8fbd1..0e17dcc9 100644 --- a/Documentation/guides/services/DependencyInjection/ExampleAutocompleteProvider.cs +++ b/Documentation/guides/services/DependencyInjection/DataAutocompleteProvider.cs @@ -4,11 +4,15 @@ namespace MyBot; -public class ExampleAutocompleteProvider(string[] data) : IAutocompleteProvider +public class DataAutocompleteProvider(IDataProvider dataProvider) : IAutocompleteProvider { - public ValueTask?> GetChoicesAsync(ApplicationCommandInteractionDataOption option, AutocompleteInteractionContext context) + public ValueTask?> 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)); diff --git a/Documentation/guides/services/DependencyInjection/DataModule.cs b/Documentation/guides/services/DependencyInjection/DataModule.cs new file mode 100644 index 00000000..45974d5e --- /dev/null +++ b/Documentation/guides/services/DependencyInjection/DataModule.cs @@ -0,0 +1,9 @@ +using NetCord.Services.Commands; + +namespace MyBot; + +public class DataModule(IDataProvider dataProvider) : CommandModule +{ + [Command("data")] + public string Data(int count) => string.Join(' ', dataProvider.GetData().Take(count)); +} diff --git a/Documentation/guides/services/DependencyInjection/DependencyInjection.csproj b/Documentation/guides/services/DependencyInjection/DependencyInjection.csproj index bb2e79ec..a1a8aa26 100644 --- a/Documentation/guides/services/DependencyInjection/DependencyInjection.csproj +++ b/Documentation/guides/services/DependencyInjection/DependencyInjection.csproj @@ -1,6 +1,7 @@  + Exe net8.0 enable latest @@ -10,8 +11,12 @@ - + + + + + diff --git a/Documentation/guides/services/DependencyInjection/ExampleModule.cs b/Documentation/guides/services/DependencyInjection/ExampleModule.cs deleted file mode 100644 index 8514a9a6..00000000 --- a/Documentation/guides/services/DependencyInjection/ExampleModule.cs +++ /dev/null @@ -1,9 +0,0 @@ -using NetCord.Services.Commands; - -namespace MyBot; - -public class ExampleModule(string botName) : CommandModule -{ - [Command("name")] - public string Name() => botName; -} diff --git a/Documentation/guides/services/DependencyInjection/IDataProvider.cs b/Documentation/guides/services/DependencyInjection/IDataProvider.cs new file mode 100644 index 00000000..c24ff4a8 --- /dev/null +++ b/Documentation/guides/services/DependencyInjection/IDataProvider.cs @@ -0,0 +1,11 @@ +namespace MyBot; + +public interface IDataProvider +{ + public IReadOnlyList GetData(); +} + +public class DataProvider : IDataProvider +{ + public IReadOnlyList GetData() => ["hello", "world"]; +} diff --git a/Documentation/guides/services/DependencyInjection/Program.cs b/Documentation/guides/services/DependencyInjection/Program.cs new file mode 100644 index 00000000..b9c81365 --- /dev/null +++ b/Documentation/guides/services/DependencyInjection/Program.cs @@ -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() + .AddDiscordGateway(o => o.Configuration = new() { Intents = GatewayIntents.GuildMessages | GatewayIntents.DirectMessages | GatewayIntents.MessageContent }) + .AddCommands() + .AddApplicationCommands(); + +var host = builder.Build(); + +host.AddModules(typeof(Program).Assembly); + +host.AddSlashCommand( + name: "data", + description: "Shows the data!", + (IDataProvider dataProvider, SlashCommandContext context, int count) => string.Join(' ', dataProvider.GetData() + .Take(count))); + +host.UseGatewayEventHandlers(); + +await host.RunAsync(); diff --git a/Documentation/guides/services/dependency-injection.md b/Documentation/guides/services/dependency-injection.md index f5cbc34f..d945bb22 100644 --- a/Documentation/guides/services/dependency-injection.md +++ b/Documentation/guides/services/dependency-injection.md @@ -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)] \ No newline at end of file +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)] \ No newline at end of file