MajMcCloud / TelegramBotFramework

This is a context based application framework for the C# TelegramBot library.
https://www.t.me/tgbotbase
MIT License
149 stars 43 forks source link

Improve navigation use DI and IServiceProvider #59

Open Kataane opened 7 months ago

Kataane commented 7 months ago

Hi. I am currently working on implementing automatic form call using IServiceProvider. Since in the new .NET 8 they added the ability to get services by key.

The first thing that came to mind is to create a scope at the user session level. That is, the session started, created a start form via IServiceProvider. And then there is automatic navigation by parsing the command from the user and getting the form using the IServiceProvider and the command as a key. But it is difficult to implement it now, because SessionManager and DeviceSession are closed for extension.

At the moment i have the following working implementation.

  1. Since the whole BotBase and session creation is not extensible. Making the wrapper / facade. Which will mimic BotBase.

  2. Intercept the message to see if the message is a command.

    public class BotBaseInjectable : IBotBaseInjectable
    {
    
    public BotBaseInjectable(IServiceProvider serviceProvider, BotBaseBuilder builder)
    {
    }
    
    private async Task Bb_BotCommand(object sender, BotCommandEventArgs e)
    {
        BotCommand?.Invoke(sender, e);
        if (!botBase.IsKnownBotCommand(e.Command))
        {
            return;
        }
        await using var scope = serviceProvider.CreateAsyncScope();
        var type = scope.ServiceProvider.GetRequiredKeyedService<FormBase>(e.Command).GetType();
        await DependencyInjection.Extensions.NavigateTo(e.Device.ActiveForm, type);
    }
    }
  3. Add a command attribute that specifies the command and its description

    [AttributeUsage(AttributeTargets.Class, Inherited = false)]
    public class BotCommandAttribute : Attribute
    {
    public string Command { get; }
    public string Description { get; }
    
    public BotCommandAttribute(string command, string description)
    {
        Command = command.ToLower();
        Description = description;
    }
    }
  4. Create a command-form with a new attribute

    [BotCommand(nameof(Greeting), "Greets user")]
    public class Greeting : FormBase
  5. All that's left to do is register it

    public static void BuildBotBase<TStartForm>(this IHostApplicationBuilder builder, Assembly assembly, Func<IServiceProvider, BotBaseBuilder> botBaseFactory)
    where TStartForm : FormBase
    {
    var botCommands = assembly.GetTypes()
        .Where(t => t.IsAssignableTo(typeof(FormBase)) && t.IsClass && t.GetCustomAttribute<BotCommandAttribute>() != null)
        .ToList();
    
    var botCommandScopes = new Dictionary<BotCommandScope, List<BotCommand>>();
    
    foreach (var botCommand in botCommands)
    {
        var command = botCommand.GetCustomAttribute<BotCommandAttribute>() ?? throw new ArgumentNullException(nameof(BotCommandAttribute));
        if (string.IsNullOrEmpty(command.Command) || string.IsNullOrEmpty(command.Description)) continue;
        var fullCommand = '/' + command.Command;
        builder.Services.AddKeyedScoped(typeof(FormBase), fullCommand, botCommand);
    
        Commands.Extensions.Add(botCommandScopes, command.Command, command.Description);
    }
    
    builder.Services.AddSingleton(typeof(IBotBaseInjectable), serviceProvider =>
    {
        var factory = botBaseFactory(serviceProvider);
        ((IStartFormSelectionStage)factory).WithServiceProvider<TStartForm>(serviceProvider);
        ((IBotCommandsStage)factory).CustomCommands(a =>
        {
            foreach (var botCommand in botCommandScopes)
            {
                a.Add(botCommand.Key, botCommand.Value);
            }
        });
        return new BotBaseInjectable(serviceProvider, botCommandScopes, factory);
    });
    }

It looks great and work, but it's not.

BotBaseInjectable is kludge doing this kind of navigation looks rather strange. Besides, it is hard to extend many things, for example SessionManager and DeviceSession. Maybe there are some ideas for improvement? Ready to assist in implementation.

Each step in the BotBase factory is bound to the next step, which makes it necessary to cast BotBaseBuilder to the required step.

Also, the problem with this solution is that you can't ignore StartFormFactory. And every time you intercept commands, StartForm will be displayed.

Best regards, Kataane.

MajMcCloud commented 7 months ago

Hey, looks interesting. I heared something about this at @NickChapsas (https://www.youtube.com/@nickchapsas)

But for now I have to get more into the benefits and how to implement such things.

Maybe my current "Lab" project is fitting here for a better needs, not sure yet. But please dont hesitate to give it a look: https://github.com/MajMcCloud/TelegramBotFramework/tree/Lab/Experiments/ExternalActionManager

Cheers!