diff --git a/Directory.Packages.props b/Directory.Packages.props
index 645a896..8a724a1 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,6 +4,7 @@
+
diff --git a/TgBotFrame.Commands.Help/HelpCommandController.cs b/TgBotFrame.Commands.Help/HelpCommandController.cs
index 5c9d105..0c03f90 100644
--- a/TgBotFrame.Commands.Help/HelpCommandController.cs
+++ b/TgBotFrame.Commands.Help/HelpCommandController.cs
@@ -1,7 +1,7 @@
-using System.Collections.Frozen;
-using System.Reflection;
+using System.Reflection;
using System.Resources;
using System.Text;
+using Microsoft.FeatureManagement;
using Telegram.Bot;
using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.ReplyMarkups;
@@ -14,7 +14,10 @@
namespace TgBotFrame.Commands.Help;
[CommandController(nameof(Help))]
-public class HelpCommandController(ITelegramBotClient botClient, CommandExplorerService commandExplorer)
+public class HelpCommandController(
+ ITelegramBotClient botClient,
+ CommandExplorerService commandExplorer,
+ IVariantFeatureManager? featureManager = null)
: CommandControllerBase
{
[Command(nameof(Help))]
@@ -56,8 +59,9 @@ public async Task HelpList()
text.AppendLine();
text.AppendLine();
- IEnumerable buttons = commandExplorer.Commands.Values
- .SelectMany(x => x.Keys)
+
+ IAsyncEnumerable buttons = commandExplorer.GetCommands(featureManager)
+ .SelectMany(x => x.Value.Select(y => y.Key))
.Select(x => x.DeclaringType)
.Select(x => (x?.Assembly,
x?.GetCustomAttribute()?.CategoryKey))
@@ -92,7 +96,7 @@ await botClient.SendMessage(
Context.GetUserId()!,
Resources.ResourceManager.GetString(nameof(HelpCommandController_HelpList_Description),
Context.GetCultureInfo())!,
- replyMarkup: new InlineKeyboardMarkup(buttons)
+ replyMarkup: new InlineKeyboardMarkup(await buttons.ToArrayAsync())
).ConfigureAwait(false);
}
@@ -102,8 +106,8 @@ await botClient.SendMessage(
[Command(nameof(HelpCategory))]
public async Task HelpCategory(string category)
{
- if (!commandExplorer.CategoriesCommandsNames.TryGetValue(category,
- out (Assembly assembly, FrozenSet commands) commands))
+ string[] commands = await commandExplorer.GetCategoryCommandsNames(featureManager, category).ToArrayAsync();
+ if (commands.Length == 0)
{
await botClient.SendMessage(
Context.GetUserId()!,
@@ -112,16 +116,22 @@ await botClient.SendMessage(
return;
}
- IEnumerable buttons = commands.commands.Select(x => new[]
+ IEnumerable buttons = commands.Select(x => new[]
{ InlineKeyboardButton.WithCallbackData(x, $@"/{nameof(HelpCommand)} {x}") });
- string text = (category.Length == 0
- ? Resources.ResourceManager.GetString(
+ string text;
+ if (category.Length == 0)
+ {
+ text = Resources.ResourceManager.GetString(
nameof(HelpCommandController_HelpCategory_NoCategory),
- Context.GetCultureInfo())
- : GetResourceManager(commands.assembly)?.GetString(
- CATEGORY_DESCRIPTION_PREFIX + category,
- Context.GetCultureInfo())) ?? category;
+ Context.GetCultureInfo()) ?? category;
+ }
+ else
+ {
+ text = commandExplorer.GetAssembliesForCategory(category).Select(x => GetResourceManager(x)
+ ?.GetString(CATEGORY_DESCRIPTION_PREFIX + category,
+ Context.GetCultureInfo())).FirstOrDefault(x => !string.IsNullOrEmpty(x)) ?? category;
+ }
await botClient.SendMessage(
Context.GetUserId()!,
@@ -132,9 +142,9 @@ await botClient.SendMessage(
[Command(nameof(HelpCommand))]
public async Task HelpCommand(string command)
{
- if (!commandExplorer.Commands.TryGetValue(command,
- out FrozenDictionary? methods)
- || methods.Count == 0)
+ Dictionary methods =
+ await commandExplorer.GetCommand(featureManager, command).ToDictionaryAsync();
+ if (methods.Count == 0)
{
await botClient.SendMessage(
Context.GetUserId()!,
diff --git a/TgBotFrame.Commands/Middleware/CommandRouterMiddleware.cs b/TgBotFrame.Commands/Middleware/CommandRouterMiddleware.cs
index 3554d9e..09930eb 100644
--- a/TgBotFrame.Commands/Middleware/CommandRouterMiddleware.cs
+++ b/TgBotFrame.Commands/Middleware/CommandRouterMiddleware.cs
@@ -1,9 +1,9 @@
-using System.Collections.Frozen;
-using System.Globalization;
+using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Microsoft.FeatureManagement;
using Telegram.Bot;
using TgBotFrame.Commands.Extensions;
using TgBotFrame.Commands.Properties;
@@ -15,7 +15,8 @@ public sealed class CommandRouterMiddleware(
CommandExplorerService commandExplorerService,
IServiceScopeFactory scopeFactory,
ITelegramBotClient botClient,
- ILogger logger) : FrameMiddleware
+ ILogger logger,
+ IVariantFeatureManager? featureManager = null) : FrameMiddleware
{
public const string COMMAND_CONTROLLER_KEY = "CommandController";
public const string COMMAND_METHOD_KEY = "CommandMethod";
@@ -38,8 +39,11 @@ public override async Task InvokeAsync(Update update, FrameContext context, Canc
ExceptedName = context.GetBotUsername();
CommandArgumentsRaw = context.GetCommandArgsRaw();
- if (!commandExplorerService.Commands.TryGetValue(CommandKey,
- out FrozenDictionary? allMethods) || allMethods.Count == 0)
+
+ Dictionary allMethods = await commandExplorerService
+ .GetCommand(featureManager, CommandKey).ToDictionaryAsync(cancellationToken: ct);
+
+ if (allMethods.Count == 0)
{
await SendCommandNotFound(update, context, ct).ConfigureAwait(false);
await Next(update, context, ct).ConfigureAwait(false);
@@ -180,7 +184,7 @@ await botClient.SendMessage(update.Message!.Chat,
}
private (MethodInfo?, int invalidArgIndex) GetMethod(
- in FrozenDictionary allMethods, out object?[] args)
+ in IDictionary allMethods, out object?[] args)
{
args = [];
KeyValuePair method;
diff --git a/TgBotFrame.Commands/Services/CommandExplorerService.cs b/TgBotFrame.Commands/Services/CommandExplorerService.cs
index 9556f63..20ce49a 100644
--- a/TgBotFrame.Commands/Services/CommandExplorerService.cs
+++ b/TgBotFrame.Commands/Services/CommandExplorerService.cs
@@ -1,23 +1,86 @@
-using System.Collections.Frozen;
+using Microsoft.Extensions.Logging;
+using Microsoft.FeatureManagement;
+using System.Collections.Frozen;
using System.Reflection;
-using Microsoft.Extensions.Logging;
using TgBotFrame.Commands.Attributes;
+using TgBotFrame.Services;
namespace TgBotFrame.Commands.Services;
public sealed class CommandExplorerService(ILogger logger)
{
- public FrozenDictionary> Commands { get; private set; } =
+ private FrozenDictionary> COMMANDS =
FrozenDictionary>.Empty;
+ private FrozenDictionary methods, Assembly[] assemblies)> CATEGORIES =
+ FrozenDictionary methods, Assembly[] assemblies)>.Empty;
- public FrozenDictionary)> CategoriesCommandsNames { get; private set; } =
- FrozenDictionary)>.Empty;
+ public async IAsyncEnumerable> GetCommand(IVariantFeatureManager? featureManager, string commandName)
+ {
+ if (!COMMANDS.TryGetValue(commandName, out FrozenDictionary? commands))
+ {
+ yield break;
+ }
+
+ if (featureManager is null)
+ {
+ foreach (KeyValuePair method in commands)
+ {
+ yield return method;
+ }
+ }
+ else
+ {
+ foreach (KeyValuePair method in commands)
+ {
+ if (await BotService.IsFeatureEnabled(featureManager, method.Key).ConfigureAwait(false))
+ yield return method;
+ }
+ }
+ }
+
+ public async IAsyncEnumerable>>> GetCommands(IVariantFeatureManager? featureManager)
+ {
+ foreach (KeyValuePair> commandName in COMMANDS)
+ {
+ yield return new(commandName.Key, GetCommand(featureManager, commandName.Key));
+ }
+ }
+
+
+
+ public IReadOnlyCollection GetAssembliesForCategory(in string category)
+ {
+ return CATEGORIES.TryGetValue(category, out (FrozenDictionary methods, Assembly[] assemblies) value) ? value.assemblies : [];
+ }
+
+ public async IAsyncEnumerable GetCategoryCommandsNames(IVariantFeatureManager? featureManager, string category)
+ {
+ if (!CATEGORIES.TryGetValue(category, out (FrozenDictionary methods, Assembly[] assemblies) methods))
+ {
+ yield break;
+ }
+
+ foreach (KeyValuePair method in methods.methods)
+ {
+ foreach (MethodInfo overload in method.Value)
+ {
+ if (!await BotService.IsFeatureEnabled(featureManager, overload))
+ {
+ continue;
+ }
+
+ yield return method.Key;
+ break;
+ }
+ }
+ }
public void FillControllers(in IEnumerable commandControllers)
{
- Dictionary> controllers = [];
- Dictionary> categories = [];
- Dictionary assemblies = [];
+ Dictionary> commands = [];
+ Dictionary> methods, List assemblies)> categories =
+ [];
+
foreach (Type type in commandControllers.Distinct())
{
CommandControllerAttribute? controllerAttribute = type.GetCustomAttribute();
@@ -25,12 +88,17 @@ public void FillControllers(in IEnumerable commandControllers)
{
continue;
}
+ Assembly assembly = type.Assembly;
- if (!categories.TryGetValue(controllerAttribute.CategoryKey, out List? categoryMethods))
+
+ if (categories.TryGetValue(controllerAttribute.CategoryKey, out (Dictionary> methods, List assemblies) categoryInfo))
+ {
+ if (!categoryInfo.assemblies.Contains(assembly)) categoryInfo.assemblies.Add(assembly);
+ }
+ else
{
- categoryMethods = [];
- categories.Add(controllerAttribute.CategoryKey, categoryMethods);
- assemblies.Add(controllerAttribute.CategoryKey, type.Assembly);
+ categories.Add(controllerAttribute.CategoryKey, ([], [assembly]));
+ categoryInfo = categories[controllerAttribute.CategoryKey];
}
foreach (MethodInfo methodInfo in type.GetMethods())
@@ -41,32 +109,31 @@ public void FillControllers(in IEnumerable commandControllers)
continue;
}
- string name = attribute.Name;
- categoryMethods.Add(name);
- if (controllers.TryGetValue(name, out List? methods))
+ if (!categoryInfo.methods.TryAdd(attribute.Name, [methodInfo]))
{
- if (!methods.Contains(methodInfo))
- {
- methods.Add(methodInfo);
- }
+ categoryInfo.methods[attribute.Name].Add(methodInfo);
}
- else
+
+ if (!commands.TryAdd(attribute.Name, [methodInfo]))
{
- controllers.Add(name, [methodInfo]);
+ var overloads = commands[attribute.Name];
+ if (!overloads.Contains(methodInfo)) overloads.Add(methodInfo);
}
}
+
}
- Commands = controllers.ToFrozenDictionary(
+ COMMANDS = commands.ToFrozenDictionary(
x => x.Key,
x => x.Value.ToFrozenDictionary(
y => y,
y => y.GetParameters()),
StringComparer.OrdinalIgnoreCase);
- CategoriesCommandsNames = categories.ToFrozenDictionary(x => x.Key,
- x => (assemblies[x.Key], x.Value.Distinct(StringComparer.OrdinalIgnoreCase).ToFrozenSet()));
+ CATEGORIES = categories.ToFrozenDictionary(x => x.Key,
+ x => (x.Value.methods.ToFrozenDictionary(y => y.Key, y => y.Value.ToArray()),
+ x.Value.assemblies.ToArray()));
logger.LogInformation(@"{all_count:D} overload(s) from {count:D} command(s) loaded",
- Commands.Sum(x => x.Value.Count), Commands.Count);
+ COMMANDS.Sum(x => x.Value.Count), COMMANDS.Count);
}
}
\ No newline at end of file
diff --git a/TgBotFrame.Example/ExtendedHelpController.cs b/TgBotFrame.Example/ExtendedHelpController.cs
index 4125424..5201722 100644
--- a/TgBotFrame.Example/ExtendedHelpController.cs
+++ b/TgBotFrame.Example/ExtendedHelpController.cs
@@ -1,19 +1,25 @@
-using Telegram.Bot;
+using Microsoft.FeatureManagement;
+using Microsoft.FeatureManagement.Mvc;
+using Telegram.Bot;
using Telegram.Bot.Types.Enums;
+using TgBotFrame.Commands;
using TgBotFrame.Commands.Attributes;
using TgBotFrame.Commands.Extensions;
-using TgBotFrame.Commands.Help;
using TgBotFrame.Commands.Services;
namespace TgBotFrame.Example;
[CommandController("Help")]
-public class ExtendedHelpController(ITelegramBotClient botClient, CommandExplorerService commandExplorer)
- : HelpCommandController(botClient, commandExplorer)
+public class ExtendedHelpController(
+ ITelegramBotClient botClient,
+ CommandExplorerService commandExplorer,
+ IVariantFeatureManager? featureManager = null)
+ : CommandControllerBase //: HelpCommandController(botClient, commandExplorer, featureManager)
{
private readonly ITelegramBotClient _botClient = botClient;
[Command(nameof(EchoDouble))]
+ [FeatureGate("EchoDoubleFeature")]
public async Task EchoDouble(string text)
{
int? messageId = Context.GetMessageId();
diff --git a/TgBotFrame.Example/Program.cs b/TgBotFrame.Example/Program.cs
index 84140a5..00bac29 100644
--- a/TgBotFrame.Example/Program.cs
+++ b/TgBotFrame.Example/Program.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using Microsoft.EntityFrameworkCore;
+using Microsoft.FeatureManagement;
using OpenTelemetry.Metrics;
using Telegram.Bot;
using TgBotFrame.Commands.Authorization.Extensions;
@@ -39,6 +40,8 @@
const string sqliteConnectionString = "Data Source=../data/sqlite/example.sqlite";
string? tgToken = builder.Configuration.GetConnectionString("Telegram");
+builder.Services.AddFeatureManagement();
+
builder.Services.AddTelegramHttpClient();
builder.Services.AddSingleton(provider =>
{
diff --git a/TgBotFrame.Example/appsettings.json b/TgBotFrame.Example/appsettings.json
index b19248a..a2ec70a 100644
--- a/TgBotFrame.Example/appsettings.json
+++ b/TgBotFrame.Example/appsettings.json
@@ -1,16 +1,24 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- },
- "ConnectionStrings": {
- "Telegram": ""
- },
- "RateLimit": {
- "Limit": 1,
- "Interval": "00:00:01"
- },
- "AllowedHosts": "*"
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "ConnectionStrings": {
+ "Telegram": ""
+ },
+ "RateLimit": {
+ "Limit": 1,
+ "Interval": "00:00:01"
+ },
+ "AllowedHosts": "*",
+ "feature_management": {
+ "feature_flags": [
+ {
+ "id": "EchoDoubleFeature",
+ "enabled": false
+ }
+ ]
+ }
}
\ No newline at end of file
diff --git a/TgBotFrame/Services/BotService.cs b/TgBotFrame/Services/BotService.cs
index 2923279..eb05c03 100644
--- a/TgBotFrame/Services/BotService.cs
+++ b/TgBotFrame/Services/BotService.cs
@@ -1,7 +1,10 @@
-using System.Runtime.CompilerServices;
+using System.Reflection;
+using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using Microsoft.FeatureManagement;
+using Microsoft.FeatureManagement.Mvc;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types.Enums;
@@ -59,7 +62,27 @@ private async Task RunMiddleware(Update update, CancellationToken cancellationTo
frameMetricsService.IncUpdatesHandled(update.Type);
AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
await using ConfiguredAsyncDisposable _ = scope.ConfigureAwait(false);
- FrameMiddleware[] middlewares = scope.ServiceProvider.GetServices().ToArray();
+
+ IVariantFeatureManager? featureManager = scope.ServiceProvider.GetService();
+ FrameMiddleware[] middlewares;
+ if (featureManager is null)
+ {
+ middlewares = scope.ServiceProvider.GetServices().ToArray();
+ }
+ else
+ {
+ List frameMiddlewares = [];
+ foreach (FrameMiddleware frameMiddleware in scope.ServiceProvider.GetServices())
+ {
+ if (await IsFeatureEnabled(featureManager, frameMiddleware.GetType()))
+ {
+ frameMiddlewares.Add(frameMiddleware);
+ }
+ }
+
+ middlewares = frameMiddlewares.ToArray();
+ }
+
if (middlewares.Length == 0)
{
logger.LogWarning(@"There is no registered middlewares, skip update processing");
@@ -77,4 +100,53 @@ private async Task RunMiddleware(Update update, CancellationToken cancellationTo
using FrameContext context = new();
await firstMiddleware.InvokeAsync(update, context, cancellationToken).ConfigureAwait(false);
}
+
+ private static async ValueTask IsFeatureEnabled(IVariantFeatureManager featureManager, Type type)
+ {
+ if (type.GetCustomAttribute() is not { } attribute)
+ {
+ return true;
+ }
+
+ return await IsFeatureEnabled(featureManager, attribute);
+ }
+
+ private static async ValueTask IsFeatureEnabled(IVariantFeatureManager? featureManager,
+ FeatureGateAttribute? attribute)
+ {
+ if (featureManager is null) return true;
+ switch (attribute?.RequirementType)
+ {
+ case RequirementType.Any:
+ foreach (string? feature in attribute.Features)
+ {
+ bool result = await featureManager.IsEnabledAsync(feature);
+ if (result)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ case RequirementType.All:
+ foreach (string? feature in attribute.Features)
+ {
+ bool result = await featureManager.IsEnabledAsync(feature);
+ if (!result)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ case null:
+ return true;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ public static async Task IsFeatureEnabled(IVariantFeatureManager? featureManager, MethodInfo method) =>
+ await IsFeatureEnabled(featureManager, method.DeclaringType?.GetCustomAttribute())
+ && await IsFeatureEnabled(featureManager, method.GetCustomAttribute());
}
\ No newline at end of file
diff --git a/TgBotFrame/TgBotFrame.csproj b/TgBotFrame/TgBotFrame.csproj
index 090d4da..4060219 100644
--- a/TgBotFrame/TgBotFrame.csproj
+++ b/TgBotFrame/TgBotFrame.csproj
@@ -33,6 +33,7 @@
+