Skip to content

Instantly share code, notes, and snippets.

@fubar-coder
Created April 8, 2026 08:12
Show Gist options
  • Select an option

  • Save fubar-coder/b5bfa42a7b4e9ef121d40f6bb35c69bf to your computer and use it in GitHub Desktop.

Select an option

Save fubar-coder/b5bfa42a7b4e9ef121d40f6bb35c69bf to your computer and use it in GitHub Desktop.
Experiment: M.E.Hosting + Consolonia/Avalonia + ReactiveUI.Avalonia

Test for using M.E.Hosting + Consolonia/Avalonia + ReactiveUI.Avalonia

This are the findings of my test:

Splat DI

Redirecting to M.E.DependencyInjection works, but has an important fallacy: It creates its own IServiceProvider instance, which will break our neck when used in combination with Consolonia/Avalonia.

Splat Logging

Sucks hard, because it requires an ILoggerFactory instance when using the UseMicrosoftExtensionsLoggingWithWrappingFullLogger extension method. It should use the following code instead:

AppLocator.CurrentMutable.RegisterLazySingleton<ILogManager>(() =>
{
    var loggerFactory = AppLocator.Current.GetService<ILoggerFactory>() ?? throw new InvalidOperationException();
    return new FuncLogManager(type =>
    {
        var actualLogger = loggerFactory.CreateLogger(type.ToString());
        var miniLoggingWrapper = new MicrosoftExtensionsLoggingLogger(actualLogger);
        return new WrappingFullLogger(miniLoggingWrapper);
    });
});

The code above pulls the ILoggerFactory from the DI container.

Consolonia/Avalonia

Consolonia requires a custom lifetime, which can only be set during a "Setup" operation, which in turn initializes the whole Avalonia system, which - in combination with ReactiveUI - will break our neck later.

ReactiveUI.Avalonia

During the initialization of ReactiveUI for Avalonia, which requires the new RxAppBuilder, several services are registered, which has several implications as can be read in the "Results" section.

Results

The combination is unusable due to several requirements which make proper integration of M.E.Hosting impossible:

  • RxAppBuilder is only called during Avalonia Setup
  • DI container must not be read-only yet for ReactiveUI services to be added
  • Avalonia/Consolonia setup causes instantiation of App class
  • The instantiation of the App class causes the Instanatiation of MainWindow und this MainViewModel
  • The instantiation uses Splat DI
  • The instantiation with Splat DI creates a temporary IServiceProvider
  • This results in a different IServiceProvider for the App/MainWindow/MainViewModel classes than the IServiceProvider of IHost
  • This in turn results in different singleton services for App/MainWindow/MainViewModel and IHost.Services
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:console="https://github.com/jinek/consolonia"
x:Class="TestHosting.App"
RequestedThemeVariant="Default">
<Application.Styles>
<console:TurboVisionElegantTheme />
</Application.Styles>
</Application>
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Splat;
namespace TestHosting;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
desktopLifetime.MainWindow = AppLocator.Current.GetService<MainWindow>();
}
base.OnFrameworkInitializationCompleted();
}
}
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Avalonia;
using Avalonia.Logging;
using Microsoft.Extensions.Logging;
namespace TestHosting;
[SuppressMessage("ReSharper", "TemplateIsNotCompileTimeConstantProblem")]
[SuppressMessage("Usage", "CA2254:Vorlage muss ein statischer Ausdruck sein")]
public class AvaloniaExtensionsLoggingSink(ILoggerFactory loggerFactory) : ILogSink
{
private readonly ConcurrentDictionary<string, ILogger> _loggers = new();
public bool IsEnabled(LogEventLevel level, string area) =>
GetLogger(area).IsEnabled(ToLogLevel(level));
public void Log(LogEventLevel level, string area, object? source, string messageTemplate) =>
Log(level, area, source, messageTemplate, []);
public void Log(LogEventLevel level, string area, object? source, string messageTemplate, params object?[] propertyValues)
{
var logger = GetLogger(area);
var logLevel = ToLogLevel(level);
if (source is null)
{
logger.Log(logLevel, messageTemplate, propertyValues);
}
else
{
var sourceAnchor = source is StyledElement { Name: not null } se
? se.Name
: source.GetHashCode().ToString("X8");
var sourceValue = $"{source.GetType().Name} #{sourceAnchor}";
logger.Log(logLevel, messageTemplate + " ({Source})", [..propertyValues, sourceValue]);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static LogLevel ToLogLevel(LogEventLevel level) => level switch
{
LogEventLevel.Verbose => LogLevel.Trace,
LogEventLevel.Debug => LogLevel.Debug,
LogEventLevel.Information => LogLevel.Information,
LogEventLevel.Warning => LogLevel.Warning,
LogEventLevel.Error => LogLevel.Error,
LogEventLevel.Fatal => LogLevel.Critical,
_ => LogLevel.None,
};
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ILogger GetLogger(string area) =>
_loggers.GetOrAdd(area, static (path, factory) => factory.CreateLogger($"Avalonia.{path}"), loggerFactory);
}
using Avalonia.Controls.ApplicationLifetimes;
using Microsoft.Extensions.Hosting;
namespace TestHosting;
public class AvaloniaHostLifetime : IHostLifetime
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly CancellationTokenRegistration _stopRequestedRegistration;
private IControlledApplicationLifetime? _avaloniaApplicationLifetime;
public AvaloniaHostLifetime(IHostApplicationLifetime hostApplicationLifetime)
{
_hostApplicationLifetime = hostApplicationLifetime;
_stopRequestedRegistration = hostApplicationLifetime.ApplicationStopping.Register(ApplicationStopRequested);
}
public void ConnectToAvalonia(IControlledApplicationLifetime avaloniaAapplicationLifetime)
{
_avaloniaApplicationLifetime = avaloniaAapplicationLifetime;
_avaloniaApplicationLifetime.Exit += (_, args) =>
{
_stopRequestedRegistration.Dispose();
Environment.ExitCode = args.ApplicationExitCode;
_hostApplicationLifetime.StopApplication();
};
}
public Task WaitForStartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private void ApplicationStopRequested()
{
_avaloniaApplicationLifetime?.Shutdown();
}
}
using System.Reactive;
using System.Reactive.Linq;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
namespace TestHosting;
public partial class MainViewModel : ReactiveObject
{
public Interaction<string, Unit> AppendOutputInteraction { get; } = new(RxSchedulers.MainThreadScheduler);
public Interaction<Unit, Unit> ClearUserInputInteraction { get; } = new(RxSchedulers.MainThreadScheduler);
[ReactiveCommand]
private void ExitApp()
{
var lifetime = (IControlledApplicationLifetime)Application.Current!.ApplicationLifetime!;
lifetime.Shutdown();
}
[ReactiveCommand]
private async Task SubmitUserInputAsync(string userInput)
{
var output = $"Received: {userInput}";
await ClearUserInputInteraction.Handle(Unit.Default);
await AppendOutputInteraction.Handle(output);
}
}
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="using:TestHosting"
xmlns:brushes="https://github.com/consolonia"
x:Class="TestHosting.MainWindow"
x:DataType="viewModels:MainViewModel"
x:CompileBindings="True"
RequestedThemeVariant="Dark"
Title="Consolonia"
Loaded="Control_OnLoaded">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Menu>
<MenuItem Header="_File">
<MenuItem Header="E_xit" Command="{Binding ExitAppCommand}" />
</MenuItem>
</Menu>
<TextBox Name="Output" Grid.Row="1" IsReadOnly="True" TextWrapping="Wrap" />
<Separator Grid.Row="2" />
<TextBox Grid.Row="3" Name="UserInput" TextWrapping="Wrap" AcceptsReturn="True">
<TextBox.KeyBindings>
<!--
<KeyBinding
Gesture="Enter"
Command="{Binding Path=SubmitUserInputCommand}"
CommandParameter="{Binding #UserInput.Text}" />
-->
<KeyBinding
Gesture="Ctrl+Enter"
Command="{Binding Path=SubmitUserInputCommand}"
CommandParameter="{Binding #UserInput.Text}" />
</TextBox.KeyBindings>
</TextBox>
</Grid>
</Window>
using System.Reactive;
using System.Reactive.Disposables.Fluent;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
namespace TestHosting;
[IViewFor<MainViewModel>]
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ViewModel = Splat.AppLocator.Current.GetService<MainViewModel>();
this.WhenActivated(disposables =>
{
this.BindInteraction(
ViewModel,
vm => vm.AppendOutputInteraction,
async context =>
{
await AppendOutputAsync(context.Input);
context.SetOutput(Unit.Default);
})
.DisposeWith(disposables);
this.BindInteraction(
ViewModel,
vm => vm.ClearUserInputInteraction,
async context =>
{
await ClearUserInputAsync();
context.SetOutput(Unit.Default);
})
.DisposeWith(disposables);
/*
Observable.FromEventPattern<KeyEventArgs>(
handler => UserInput.AddHandler(KeyDownEvent, handler, RoutingStrategies.Tunnel),
handler => UserInput.RemoveHandler(KeyDownEvent, handler))
.Subscribe(x =>
{
System.Diagnostics.Debug.WriteLine($"Key = {x.EventArgs.Key}, Modifiers = {x.EventArgs.KeyModifiers}");
var isShiftEnter = x.EventArgs is { KeyModifiers: KeyModifiers.Shift or KeyModifiers.Alt, Key: Key.Enter };
if (!isShiftEnter)
return;
x.EventArgs.Handled = true;
InsertNewLine();
})
.DisposeWith(disposables);
*/
});
}
private Task AppendOutputAsync(string text)
{
var updatedText = (Output.Text ?? string.Empty) + text + "\n";
Output.Text = updatedText;
Output.CaretIndex = updatedText.Length;
var lineCount = UserInput.GetLineCount();
if (lineCount != 0)
UserInput.ScrollToLine(lineCount - 1);
return Task.CompletedTask;
}
private void InsertNewLine()
{
var (start, end) = UserInput.SelectionStart > UserInput.SelectionEnd
? (UserInput.SelectionEnd, UserInput.SelectionStart)
: (UserInput.SelectionStart, UserInput.SelectionEnd);
var originalText = UserInput.Text ?? string.Empty;
var updatedText = originalText[..start] + UserInput.NewLine + originalText[end..];
UserInput.Text = updatedText;
UserInput.CaretIndex = start + UserInput.NewLine.Length;
}
private Task ClearUserInputAsync()
{
UserInput.Text = string.Empty;
return Task.CompletedTask;
}
private void Control_OnLoaded(object? sender, RoutedEventArgs e)
{
UserInput.Focus();
}
}
using Avalonia.Logging;
using Consolonia;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ReactiveUI.Avalonia;
using Splat;
using Splat.Microsoft.Extensions.DependencyInjection;
using Splat.Microsoft.Extensions.Logging;
using TestHosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Logging
.ClearProviders()
.AddDebug();
builder.Services
.AddSingleton<AvaloniaExtensionsLoggingSink>()
.AddSingleton<AvaloniaHostLifetime>()
.AddSingleton<IHostLifetime>(sp => sp.GetRequiredService<AvaloniaHostLifetime>())
.AddSingleton<App>()
.AddSingleton<MainWindow>()
.AddSingleton<MainViewModel>();
builder.Services.UseMicrosoftDependencyResolver();
AppLocator.CurrentMutable.InitializeSplat();
// Make Splat use M.E.Logging
AppLocator.CurrentMutable.RegisterLazySingleton<ILogManager>(() =>
{
var loggerFactory = AppLocator.Current.GetService<ILoggerFactory>() ?? throw new InvalidOperationException();
return new FuncLogManager(type =>
{
var actualLogger = loggerFactory.CreateLogger(type.ToString());
var miniLoggingWrapper = new MicrosoftExtensionsLoggingLogger(actualLogger);
return new WrappingFullLogger(miniLoggingWrapper);
});
});
// Build Avalonia App that, which requires service registration
var avaloniaAppBuilder = BuildAvaloniaApp();
var avaloniaLifetime = ApplicationStartup.CreateLifetime(avaloniaAppBuilder, args);
using var host = builder.Build();
// Make Consolonia/Avalonia lifetime available for M.E.Hosting
var hostLifetime = host.Services.GetRequiredService<AvaloniaHostLifetime>();
hostLifetime.ConnectToAvalonia(avaloniaLifetime);
// Make Splat use M.E.DependencyInjection services
host.Services.UseMicrosoftDependencyResolver();
// Change Avalonia logging to M.E.Logging
Logger.Sink = host.Services.GetRequiredService<AvaloniaExtensionsLoggingSink>();
await host.StartAsync().ConfigureAwait(false);
try
{
return avaloniaLifetime.Start(args);
}
finally
{
await host.StopAsync().ConfigureAwait(false);
}
static Avalonia.AppBuilder BuildAvaloniaApp()
{
return Avalonia.AppBuilder.Configure(() => Locator.Current.GetService<App>() ?? throw new InvalidOperationException())
.UseConsolonia()
.UseAutoDetectedConsole()
.UseReactiveUI(rxAppBuilder => { })
.RegisterReactiveUIViewsFromAssemblyOf<MainWindow>();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment