From eb76069adf1166c95daf31eee1fcfc4984f68c1a Mon Sep 17 00:00:00 2001 From: FrigaT Date: Sat, 6 Sep 2025 00:07:08 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D1=8C=D1=82?= =?UTF-8?q?=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ManualRelease.yaml | 132 ++++++++++++++++++ Directory.Build.props | 22 +++ PipelineFramework.sln | 43 ++++++ README.md | 55 ++++++++ .../IPipeline.cs | 6 + .../IPipelineHook.cs | 7 + .../IPipelineMiddleware.cs | 6 + .../IPipelineModule.cs | 8 ++ .../PipelineFramework.Abstractions.csproj | 9 ++ .../PipelineFramework.DI.csproj | 17 +++ .../PipelineServiceCollectionExtensions.cs | 20 +++ .../Abstractions/IPipeline.cs | 6 + .../Abstractions/IPipelineHook.cs | 8 ++ .../Abstractions/IPipelineMiddleware.cs | 6 + .../Abstractions/IPipelineModule.cs | 9 ++ src/PipelineFramework/Core/Pipeline.cs | 70 ++++++++++ src/PipelineFramework/Core/PipelineBuilder.cs | 38 +++++ .../PipelineFramework.csproj | 9 ++ 18 files changed, 471 insertions(+) create mode 100644 .gitea/workflows/ManualRelease.yaml create mode 100644 Directory.Build.props create mode 100644 PipelineFramework.sln create mode 100644 README.md create mode 100644 src/PipelineFramework.Abstractions/IPipeline.cs create mode 100644 src/PipelineFramework.Abstractions/IPipelineHook.cs create mode 100644 src/PipelineFramework.Abstractions/IPipelineMiddleware.cs create mode 100644 src/PipelineFramework.Abstractions/IPipelineModule.cs create mode 100644 src/PipelineFramework.Abstractions/PipelineFramework.Abstractions.csproj create mode 100644 src/PipelineFramework.DI/PipelineFramework.DI.csproj create mode 100644 src/PipelineFramework.DI/PipelineServiceCollectionExtensions.cs create mode 100644 src/PipelineFramework/Abstractions/IPipeline.cs create mode 100644 src/PipelineFramework/Abstractions/IPipelineHook.cs create mode 100644 src/PipelineFramework/Abstractions/IPipelineMiddleware.cs create mode 100644 src/PipelineFramework/Abstractions/IPipelineModule.cs create mode 100644 src/PipelineFramework/Core/Pipeline.cs create mode 100644 src/PipelineFramework/Core/PipelineBuilder.cs create mode 100644 src/PipelineFramework/PipelineFramework.csproj diff --git a/.gitea/workflows/ManualRelease.yaml b/.gitea/workflows/ManualRelease.yaml new file mode 100644 index 0000000..33e62dc --- /dev/null +++ b/.gitea/workflows/ManualRelease.yaml @@ -0,0 +1,132 @@ +name: Ручное создание релиза +run-name: ${{ gitea.actor }} запустил создание релиза +on: + workflow_dispatch: + inputs: + version_type: + description: 'Тип версии' + required: true + type: choice + options: + - major + - minor + - patch + default: 'patch' + pre_release: + description: "Отметка pre-release (для не master веток всегда true)" + type: boolean + default: false +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + dotnet: [ '8.0.x' ] + + name: .Net ${{ matrix.dotnet }} Release + steps: + - name: Получение исходников + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Полная история для тегов + + - name: Автоматическое определение pre-release + id: pre-release-detector + run: | + # Определяем ветку по умолчанию (main/master) + DEFAULT_BRANCH=$(git remote show origin | grep 'HEAD branch' | awk '{print $3}') + echo "Default branch: $DEFAULT_BRANCH" + + # Если текущая ветка не дефолтная - форсируем pre-release + if [ "$GITHUB_REF_NAME" != "$DEFAULT_BRANCH" ]; then + echo "Ветка не master - устанавливаем pre-release" + echo "PRE_RELEASE=true" >> $GITHUB_ENV + else + echo "Ветка master - используем pre-release из параметров" + echo "PRE_RELEASE=${{ github.event.inputs.pre_release }}" >> $GITHUB_ENV + fi + + - name: Получение последней версии + id: versioning + run: | + # Получаем последний тег + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") + echo "Последний тэг: $LATEST_TAG" + + # Извлекаем цифры версии + VERSION="${LATEST_TAG#v}" + + if [[ ! $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-.+)?$ ]]; then + echo "Неверный формат версии: $VERSION" + exit 1 + fi + + MAJOR=${BASH_REMATCH[1]} + MINOR=${BASH_REMATCH[2]} + PATCH=${BASH_REMATCH[3]} + + # Увеличиваем версию + case "${{ github.event.inputs.version_type }}" in + major) + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + patch) + PATCH=$((PATCH + 1)) + ;; + esac + + BASE_VERSION="$MAJOR.$MINOR.$PATCH" + + # Добавим pre-release, если требуется + if ${{ env.PRE_RELEASE }}; then + TIMESTAMP=$(date -u +"%Y%m%d%H%M%S") + NEW_VERSION="$BASE_VERSION-pre.$TIMESTAMP" + else + NEW_VERSION="$BASE_VERSION" + fi + + echo "Новая версия: $NEW_VERSION" + echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "VERSION=$NEW_VERSION" >> $GITHUB_ENV + + - name: Настройка .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ matrix.dotnet }} + + - name: Установка версии RetailUpdatesBot + run: | + sed -i "s/.*<\/Version>/${{ env.VERSION }}<\/Version>/g" ./RetailUpdatesBot/RetailUpdatesBot.csproj + + - name: Восстановление зависимостей + run: dotnet restore --nologo + + - name: Сборка решения + run: dotnet build --no-restore --nologo + + - name: Публикация + run: dotnet publish RetailUpdatesBot --configuration Release --runtime win-x64 --artifacts-path artifacts --nologo + + - name: Создание ZIP архива + run: | + cd artifacts/publish/RetailUpdatesBot/release_win-x64/ + zip -r ../RetailUpdatesBot-v${{ env.VERSION }}.zip * + cd .. + echo "ZIP_PATH=RetailUpdatesBot-v${{ env.VERSION }}.zip" >> $GITHUB_ENV + + - name: Создание релиза + uses: https://gitea.com/actions/gitea-release-action@v1 + env: + NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18 + with: + files: artifacts/publish/RetailUpdatesBot/RetailUpdatesBot-v${{ env.VERSION }}.zip + tag_name: v${{ env.VERSION }} + name: RetailUpdatesBot-v${{ env.VERSION }} + body: "## Что нового\n\n- Описание изменений\n- Функциональность\n- Исправления" + prerelease: ${{ env.PRE_RELEASE }} \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..46ce19a --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,22 @@ + + + + net9.0 + latest + enable + true + CS1591 + + + FrigaT + PipelineFramework + Гибкий и лёгкий фреймворк конвейеров для .NET-приложений + https://github.com/MaxLabs/PipelineFramework + https://github.com/MaxLabs/PipelineFramework + git + MIT + pipeline;framework;middleware;dotnet + 0.0.1 + true + + \ No newline at end of file diff --git a/PipelineFramework.sln b/PipelineFramework.sln new file mode 100644 index 0000000..9c16a7f --- /dev/null +++ b/PipelineFramework.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36414.22 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipelineFramework", "src\PipelineFramework\PipelineFramework.csproj", "{4407AD88-6FFC-47CF-B06A-BC1D9E05B7E5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipelineFramework.DI", "src\PipelineFramework.DI\PipelineFramework.DI.csproj", "{B5D47E22-BD13-458B-ACDC-2017C962AE80}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Элементы решения", "Элементы решения", "{754FC069-D67B-A9D7-50A1-8D1CA196D8F1}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4407AD88-6FFC-47CF-B06A-BC1D9E05B7E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4407AD88-6FFC-47CF-B06A-BC1D9E05B7E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4407AD88-6FFC-47CF-B06A-BC1D9E05B7E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4407AD88-6FFC-47CF-B06A-BC1D9E05B7E5}.Release|Any CPU.Build.0 = Release|Any CPU + {B5D47E22-BD13-458B-ACDC-2017C962AE80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5D47E22-BD13-458B-ACDC-2017C962AE80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5D47E22-BD13-458B-ACDC-2017C962AE80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5D47E22-BD13-458B-ACDC-2017C962AE80}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {4407AD88-6FFC-47CF-B06A-BC1D9E05B7E5} = {36D591C7-65C7-A0D1-1CBC-10CDE441BDC8} + {B5D47E22-BD13-458B-ACDC-2017C962AE80} = {36D591C7-65C7-A0D1-1CBC-10CDE441BDC8} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {22A2D53A-92B8-4C82-A391-6A00DE84526C} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..6fef88c --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# PipelineFramework + +**PipelineFramework** — это гибкий и лёгкий фреймворк для построения конвейеров обработки данных и логики в .NET-приложениях. +Он позволяет создавать последовательности шагов (middleware), которые обрабатывают входные данные, управляют потоком выполнения и обеспечивают расширяемость. + +## 🚀 Возможности + +- Простое определение шагов конвейера +- Поддержка асинхронной обработки +- Встроенная DI-интеграция +- Расширяемость через интерфейсы +- Минимум зависимостей + +## 📦 Установка + +```bash +dotnet add package PipelineFramework +``` + +## 🧩 Пример использования + +```csharp +var pipeline = new PipelineBuilder() + .Use(async (input, next) => + { + Console.WriteLine($"Step 1: {input}"); + await next(input + " → Step1"); + }) + .Use(async (input, next) => + { + Console.WriteLine($"Step 2: {input}"); + await next(input + " → Step2"); + }) + .Build(); + +await pipeline.ExecuteAsync("Start"); +``` + +## 📚 Документация + +- [Примеры использования](docs/examples.md) +- [Интеграция с DI](docs/di.md) +- [Расширение фреймворка](docs/extending.md) + +## 🛠 Требования + +- .NET 9.0 или выше + +## 🧑‍💻 Автор + +Разработано [FrigaT](https://github.com/FrigaT) + +## 📄 Лицензия + +Проект распространяется под лицензией [MIT](LICENSE) \ No newline at end of file diff --git a/src/PipelineFramework.Abstractions/IPipeline.cs b/src/PipelineFramework.Abstractions/IPipeline.cs new file mode 100644 index 0000000..21618e6 --- /dev/null +++ b/src/PipelineFramework.Abstractions/IPipeline.cs @@ -0,0 +1,6 @@ +namespace PipelineFramework.Abstractions; + +public interface IPipeline +{ + Task ExecuteAsync(TContext context); +} diff --git a/src/PipelineFramework.Abstractions/IPipelineHook.cs b/src/PipelineFramework.Abstractions/IPipelineHook.cs new file mode 100644 index 0000000..a39bf95 --- /dev/null +++ b/src/PipelineFramework.Abstractions/IPipelineHook.cs @@ -0,0 +1,7 @@ +namespace PipelineFramework.Abstractions; + +public interface IPipelineHook +{ + Task OnBeforeExecuteAsync(TContext context); + Task OnAfterExecuteAsync(TContext context); +} diff --git a/src/PipelineFramework.Abstractions/IPipelineMiddleware.cs b/src/PipelineFramework.Abstractions/IPipelineMiddleware.cs new file mode 100644 index 0000000..0f372a5 --- /dev/null +++ b/src/PipelineFramework.Abstractions/IPipelineMiddleware.cs @@ -0,0 +1,6 @@ +namespace PipelineFramework.Abstractions; + +public interface IPipelineMiddleware +{ + Task InvokeAsync(TContext context, Func next); +} diff --git a/src/PipelineFramework.Abstractions/IPipelineModule.cs b/src/PipelineFramework.Abstractions/IPipelineModule.cs new file mode 100644 index 0000000..90d1533 --- /dev/null +++ b/src/PipelineFramework.Abstractions/IPipelineModule.cs @@ -0,0 +1,8 @@ +namespace PipelineFramework.Abstractions; + +public interface IPipelineModule +{ + string Id { get; } + IEnumerable DependsOn { get; } + Task ExecuteAsync(TContext context); +} diff --git a/src/PipelineFramework.Abstractions/PipelineFramework.Abstractions.csproj b/src/PipelineFramework.Abstractions/PipelineFramework.Abstractions.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/src/PipelineFramework.Abstractions/PipelineFramework.Abstractions.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/src/PipelineFramework.DI/PipelineFramework.DI.csproj b/src/PipelineFramework.DI/PipelineFramework.DI.csproj new file mode 100644 index 0000000..b73817d --- /dev/null +++ b/src/PipelineFramework.DI/PipelineFramework.DI.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + diff --git a/src/PipelineFramework.DI/PipelineServiceCollectionExtensions.cs b/src/PipelineFramework.DI/PipelineServiceCollectionExtensions.cs new file mode 100644 index 0000000..2b616b5 --- /dev/null +++ b/src/PipelineFramework.DI/PipelineServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using PipelineFramework; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class PipelineServiceCollectionExtensions +{ + public static IServiceCollection AddPipeline(this IServiceCollection services) + { + services.AddTransient>(provider => + { + var modules = provider.GetServices>().ToList(); + var middlewares = provider.GetServices>().ToList(); + var hooks = provider.GetServices>().ToList(); + + return new Pipeline(modules, middlewares, hooks); + }); + + return services; + } +} diff --git a/src/PipelineFramework/Abstractions/IPipeline.cs b/src/PipelineFramework/Abstractions/IPipeline.cs new file mode 100644 index 0000000..5a2617d --- /dev/null +++ b/src/PipelineFramework/Abstractions/IPipeline.cs @@ -0,0 +1,6 @@ +namespace PipelineFramework; + +public interface IPipeline +{ + Task RunAsync(TContext context); +} diff --git a/src/PipelineFramework/Abstractions/IPipelineHook.cs b/src/PipelineFramework/Abstractions/IPipelineHook.cs new file mode 100644 index 0000000..49fbbf2 --- /dev/null +++ b/src/PipelineFramework/Abstractions/IPipelineHook.cs @@ -0,0 +1,8 @@ +namespace PipelineFramework; + +public interface IPipelineHook +{ + Task OnBeforeAsync(TContext context, IPipelineModule module); + Task OnAfterAsync(TContext context, IPipelineModule module); + Task OnErrorAsync(TContext context, IPipelineModule module, Exception ex); +} diff --git a/src/PipelineFramework/Abstractions/IPipelineMiddleware.cs b/src/PipelineFramework/Abstractions/IPipelineMiddleware.cs new file mode 100644 index 0000000..7593889 --- /dev/null +++ b/src/PipelineFramework/Abstractions/IPipelineMiddleware.cs @@ -0,0 +1,6 @@ +namespace PipelineFramework; + +public interface IPipelineMiddleware +{ + Task InvokeAsync(TContext context, Func next); +} diff --git a/src/PipelineFramework/Abstractions/IPipelineModule.cs b/src/PipelineFramework/Abstractions/IPipelineModule.cs new file mode 100644 index 0000000..695ef9b --- /dev/null +++ b/src/PipelineFramework/Abstractions/IPipelineModule.cs @@ -0,0 +1,9 @@ +namespace PipelineFramework; + +public interface IPipelineModule +{ + string Id { get; } + IEnumerable DependsOn { get; } + + Task ExecuteAsync(TContext context); +} diff --git a/src/PipelineFramework/Core/Pipeline.cs b/src/PipelineFramework/Core/Pipeline.cs new file mode 100644 index 0000000..ae1937d --- /dev/null +++ b/src/PipelineFramework/Core/Pipeline.cs @@ -0,0 +1,70 @@ +namespace PipelineFramework; + +public class Pipeline : IPipeline +{ + private readonly List> _modules; + private readonly List> _middlewares; + private readonly List> _hooks; + + + public Pipeline( + List> modules, + List> middlewares, + List> hooks) + { + _modules = modules; + _middlewares = middlewares; + _hooks = hooks; + } + + public async Task RunAsync(TContext context) + { + var executed = new HashSet(); + var moduleMap = _modules.ToDictionary(m => m.Id); + + Func pipeline = async () => + { + while (executed.Count < _modules.Count) + { + var ready = _modules + .Where(m => !executed.Contains(m.Id) && + m.DependsOn.All(dep => executed.Contains(dep))) + .ToList(); + + var tasks = ready.Select(m => ExecuteModuleWithHooksAsync(m, context)).ToList(); + await Task.WhenAll(tasks); + + foreach (var m in ready) + executed.Add(m.Id); + } + }; + + foreach (var middleware in _middlewares.Reverse()) + { + var next = pipeline; + pipeline = () => middleware(context, next); + } + + await pipeline(); + } + + private async Task ExecuteModuleWithHooksAsync(IPipelineModule module, TContext context) + { + try + { + foreach (var hook in _hooks) + await hook.OnBeforeAsync(context, module); + + await module.ExecuteAsync(context); + + foreach (var hook in _hooks) + await hook.OnAfterAsync(context, module); + } + catch (Exception ex) + { + foreach (var hook in _hooks) + await hook.OnErrorAsync(context, module, ex); + throw; + } + } +} \ No newline at end of file diff --git a/src/PipelineFramework/Core/PipelineBuilder.cs b/src/PipelineFramework/Core/PipelineBuilder.cs new file mode 100644 index 0000000..6e21017 --- /dev/null +++ b/src/PipelineFramework/Core/PipelineBuilder.cs @@ -0,0 +1,38 @@ +namespace PipelineFramework; + +public abstract class PipelineBuilder +{ + private readonly List> _modules = new(); + private readonly List, Task>> _middlewares = new(); + private readonly List> _hooks = new(); + + public PipelineBuilder AddModule(IPipelineModule module) + { + _modules.Add(module); + return this; + } + + public PipelineBuilder UseMiddleware() where T : IPipelineMiddleware, new() + { + _middlewares.Add((ctx, next) => new T().InvokeAsync(ctx, next)); + return this; + } + + public PipelineBuilder UseMiddleware(Func, Task> middleware) + { + _middlewares.Add(middleware); + return this; + } + + public PipelineBuilder AddHook(IPipelineHook hook) + { + _hooks.Add(hook); + return this; + } + + protected IReadOnlyList> Modules => _modules; + protected IReadOnlyList, Task>> Middlewares => _middlewares; + protected IReadOnlyList> Hooks => _hooks; + + public abstract IPipeline Build(); +} diff --git a/src/PipelineFramework/PipelineFramework.csproj b/src/PipelineFramework/PipelineFramework.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/src/PipelineFramework/PipelineFramework.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + +