T.Molodoy.MviCore.Net 0.0.1

dotnet add package T.Molodoy.MviCore.Net --version 0.0.1
                    
NuGet\Install-Package T.Molodoy.MviCore.Net -Version 0.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="T.Molodoy.MviCore.Net" Version="0.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="T.Molodoy.MviCore.Net" Version="0.0.1" />
                    
Directory.Packages.props
<PackageReference Include="T.Molodoy.MviCore.Net" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add T.Molodoy.MviCore.Net --version 0.0.1
                    
#r "nuget: T.Molodoy.MviCore.Net, 0.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package T.Molodoy.MviCore.Net@0.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=T.Molodoy.MviCore.Net&version=0.0.1
                    
Install as a Cake Addin
#tool nuget:?package=T.Molodoy.MviCore.Net&version=0.0.1
                    
Install as a Cake Tool

T.Molodoy.MviCore.Net

Упрощённый перенос ядра Badoo MVICore на C# и R3. Библиотека на netstandard2.1, зависимость — R3 1.3.1.

Установка

dotnet add package T.Molodoy.MviCore.Net --version 0.0.1

Быстрый старт

using MviCore.Net;
using SharpExtensions;
using R3;

using var counter = new ReducerFeature<int, int>(
    initialState: 0,
    reducer: (state, delta) => state + delta);

using var subscription = counter.Subscribe(Console.WriteLine);
counter.Accept(1);
counter.Accept(2);
Console.WriteLine(counter.State);

Feature работает сразу после создания, даже без подписчиков. Новый подписчик получает текущее состояние, затем результат каждой редукции, включая равные предыдущему состояния. States позволяет подписываться через IStore / IFeature. Сам feature также является R3 Observable<TState> и поддерживает обычные операторы R3.

API

Тип Назначение
ReducerFeature<Wish, State, News> Wish сразу передаётся в Reducer как Effect
ActorReducerFeature<Wish, Effect, State, News> Actor преобразует Wish в поток Effect
BaseFeature<Wish, Action, Effect, State, News> Полная цепочка с внутренними Action и PostProcessor
IStore<Wish, State> Accept(wish), State, States
IFeature<Wish, State, News> Store плюс News, IsDisposed, Dispose()

Все три класса имеют вариант без последнего параметра News; в нём используется Nothing. Компоненты задаются делегатами, поэтому можно передавать лямбды и методы:

WishToAction:  Wish                  → Action
Bootstrapper: ()                    → Observable<Action>
Actor:        (State, Action)       → Observable<Effect>
Reducer:      (State, Effect)       → State
PostProcessor:(Action, Effect, State) → Optional<Action>
NewsPublisher:(Action, Effect, State) → Optional<News>

Обязательные параметры конструктора идут первыми; необязательные bootstrapper, postProcessor и newsPublisher удобно передавать по имени. Сохранены основные имена, порядок generic-параметров и модель исполнения оригинала. Это не двоично совместимый порт. Binder, middleware, time travel и платформенные интеграции в это ядро не входят.

Actor и новости

using var feature = new ActorReducerFeature<int, int, int, string>(
    initialState: 0,
    actor: (state, wish) => Observable.Return(wish * 2),
    reducer: (state, effect) => state + effect,
    newsPublisher: (wish, effect, state) => state >= 10
        ? $"Достигнуто {state}"
        : default(Optional<string>));

using var states = feature.Subscribe(Console.WriteLine);
using var news = feature.News.Subscribe(Console.WriteLine);
feature.Accept(5);

Actor получает состояние на момент поступления Action. Каждый его Effect применяется к последнему состоянию, в том числе если другие Actor уже успели его изменить. Потоки разных Actor могут существовать одновременно; новый Wish не отменяет предыдущие. Новости получают только текущие подписчики, без сохранения и повторной выдачи. У ReducerFeature сокращённый SimpleNewsPublisher принимает только (wish, state).

default(Optional<T>) (или default) означает отсутствие Action/News. Само значение неявно преобразуется в Optional<T>; null для ссылочного типа также означает отсутствие. Optional<T> предоставляется зависимостью T.Molodoy.SharpExtensions. Это позволяет использовать int, enum и struct: 0 и false являются полноценными событиями, а не признаком отсутствия результата. В лямбдах для отсутствия результата предпочтителен явный default(Optional<T>).

Внутренние действия и запуск

using var feature = new BaseFeature<string, int, int, int>(
    initialState: 0,
    wishToAction: int.Parse,
    actor: (_, action) => Observable.Return(action),
    reducer: (state, effect) => state + effect,
    bootstrapper: () => Observable.Return(1),
    postProcessor: (_, _, state) => state == 2 ? 10 : default(Optional<int>));

Console.WriteLine(feature.State);
feature.Accept("1");

Bootstrapper вызывается один раз в конструкторе и может возвращать длительный поток. Синхронный запуск происходит до возврата из конструктора: поля производного класса, которые инициализируются позже, ещё недоступны, а начальные новости не буферизуются. Для отложенного запуска можно вернуть внешний Subject с действиями и отправить событие после создания feature и подписки на выходы.

После Reducer новое состояние сохраняется и публикуется; затем вызывается PostProcessor, затем NewsPublisher. Оба получают именно состояние этой редукции. Вызовы синхронны: внутреннее действие из PostProcessor выполняется до публикации новости исходного действия. Повторный Accept из подписчика тоже выполняется сразу. Очереди нет; бесконечные или очень глубокие цепочки внутренних действий нужно исключить в бизнес-логике.

Потоки, ошибки и освобождение ресурсов

Все Accept, уведомления Actor/Bootstrapper, подписки и Dispose должны выполняться в одном потоке. Собственных lock, проверок thread ID, scheduler и синхронизации нет. Внутри самих примитивов R3 может использоваться синхронизация. Для асинхронных операций нужно самостоятельно вернуть уведомления в поток feature, например через ObserveOn(context), где context — нужный SynchronizationContext, перед возвратом потока из Actor.

Ошибки уведомлений следуют модели R3: OnErrorResume, неуспешное завершение Actor или Bootstrapper и исключения Reducer/PostProcessor/NewsPublisher передаются глобальному ObservableSystem.RegisterUnhandledExceptionHandler. OnErrorResume не отменяет поток; неуспешное завершение отменяет только конкретную подписку. Feature остаётся работоспособным. Исключения WishToAction и фабрики Actor при прямом Accept выходят вызывающему коду. Ошибки, являющиеся частью бизнес-логики, следует преобразовывать в Effect внутри Actor.

Dispose() отменяет все активные Actor и Bootstrapper, завершает States и News и допускает повторный вызов. После него Accept бросает ObjectDisposedException, а State сохраняет последнее значение. Подписки, завершившиеся раньше, сразу удаляются из учёта. Отписка отдельного наблюдателя не останавливает feature. Если освобождение upstream бросает исключение, остальные ресурсы всё равно освобождаются, затем выбрасывается AggregateException.

Проверка

Для тестов нужен .NET SDK 9 или новее:

dotnet test MviCore.Net.sln --configuration Release
dotnet pack MviCore.Net.R3/MviCore.Net.R3.csproj --configuration Release

Тесты проверяют актуальность состояния при чередовании Effect, порядок PostProcessor/News, bootstrap, повторный вход, обработку ошибок, освобождение подписок и отсутствие удержания Action после синхронного завершения Actor.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.0.1 43 11/8/2026