using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; using Avalonia.Input.Platform; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Styling; using Avalonia.Threading; using FluentAvalonia.Interop; using FluentAvalonia.UI.Controls; using MessagePipe; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NLog; using NLog.Config; using NLog.Extensions.Logging; using NLog.Targets; using Octokit; using Polly; using Polly.Contrib.WaitAndRetry; using Polly.Extensions.Http; using Polly.Timeout; using Refit; using Sentry; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Progress; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Configs; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Services; using Application = Avalonia.Application; using LogLevel = Microsoft.Extensions.Logging.LogLevel; #if DEBUG using StabilityMatrix.Avalonia.Diagnostics.LogViewer; using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions; #endif namespace StabilityMatrix.Avalonia; public sealed class App : Application { private static bool isAsyncDisposeComplete; [NotNull] public static IServiceProvider? Services { get; private set; } [NotNull] public static Visual? VisualRoot { get; internal set; } public static TopLevel TopLevel => TopLevel.GetTopLevel(VisualRoot)!; internal static bool IsHeadlessMode => TopLevel.TryGetPlatformHandle()?.HandleDescriptor is null or "STUB"; [NotNull] public static IStorageProvider? StorageProvider { get; internal set; } [NotNull] public static IClipboard? Clipboard { get; internal set; } // ReSharper disable once MemberCanBePrivate.Global [NotNull] public static IConfiguration? Config { get; private set; } // ReSharper disable once MemberCanBePrivate.Global public IClassicDesktopStyleApplicationLifetime? DesktopLifetime => ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; public static new App? Current => (App?)Application.Current; /// /// Called before is built. /// Can be used by UI tests to override services. /// internal static event EventHandler? BeforeBuildServiceProvider; public override void Initialize() { AvaloniaXamlLoader.Load(this); SetFontFamily(GetPlatformDefaultFontFamily()); // Set design theme if (Design.IsDesignMode) { RequestedThemeVariant = ThemeVariant.Dark; } } public override void OnFrameworkInitializationCompleted() { // Remove DataAnnotations validation plugin since we're using INotifyDataErrorInfo from MvvmToolkit var dataValidationPluginsToRemove = BindingPlugins .DataValidators.OfType() .ToArray(); foreach (var plugin in dataValidationPluginsToRemove) { BindingPlugins.DataValidators.Remove(plugin); } base.OnFrameworkInitializationCompleted(); if (Design.IsDesignMode) { DesignData.DesignData.Initialize(); Services = DesignData.DesignData.Services; } else { ConfigureServiceProvider(); } if (DesktopLifetime is not null) { DesktopLifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; Setup(); // First time setup if needed var settingsManager = Services.GetRequiredService(); if (!settingsManager.IsEulaAccepted()) { var setupWindow = Services.GetRequiredService(); var setupViewModel = Services.GetRequiredService(); setupWindow.DataContext = setupViewModel; setupWindow.ShowAsDialog = true; setupWindow.ShowActivated = true; setupWindow.ShowAsyncCts = new CancellationTokenSource(); setupWindow.ExtendClientAreaChromeHints = Program.Args.NoWindowChromeEffects ? ExtendClientAreaChromeHints.NoChrome : ExtendClientAreaChromeHints.PreferSystemChrome; DesktopLifetime.MainWindow = setupWindow; setupWindow.ShowAsyncCts.Token.Register(() => { if (setupWindow.Result == ContentDialogResult.Primary) { settingsManager.SetEulaAccepted(); ShowMainWindow(); DesktopLifetime.MainWindow.Show(); } else { Shutdown(); } }); } else { ShowMainWindow(); } } } /// /// Set the default font family for the application. /// private void SetFontFamily(FontFamily fontFamily) { Resources["ContentControlThemeFontFamily"] = fontFamily; } /// /// Get the default font family for the current platform and language. /// public FontFamily GetPlatformDefaultFontFamily() { try { var fonts = new List(); if (Cultures.Current?.Name == "ja-JP") { return Resources["NotoSansJP"] as FontFamily ?? throw new ApplicationException("Font NotoSansJP not found"); } if (Compat.IsWindows) { fonts.Add(OSVersionHelper.IsWindows11() ? "Segoe UI Variable Text" : "Segoe UI"); } else if (Compat.IsMacOS) { // Use Segoe fonts if installed, but we can't distribute them fonts.Add("Segoe UI Variable"); fonts.Add("Segoe UI"); fonts.Add("San Francisco"); fonts.Add("Helvetica Neue"); fonts.Add("Helvetica"); } else { return FontFamily.Default; } return new FontFamily(string.Join(",", fonts)); } catch (Exception e) { LogManager.GetCurrentClassLogger().Error(e); return FontFamily.Default; } } /// /// Setup tasks to be run shortly before any window is shown /// private void Setup() { using var _ = new CodeTimer(); // Setup uri handler for `stabilitymatrix://` protocol Program.UriHandler.RegisterUriScheme(); } private void ShowMainWindow() { if (DesktopLifetime is null) return; var mainViewModel = Services.GetRequiredService(); var mainWindow = Services.GetRequiredService(); mainWindow.DataContext = mainViewModel; mainWindow.ExtendClientAreaChromeHints = Program.Args.NoWindowChromeEffects ? ExtendClientAreaChromeHints.NoChrome : ExtendClientAreaChromeHints.PreferSystemChrome; var settingsManager = Services.GetRequiredService(); var windowSettings = settingsManager.Settings.WindowSettings; if (windowSettings != null && !Program.Args.ResetWindowPosition) { mainWindow.Position = new PixelPoint(windowSettings.X, windowSettings.Y); mainWindow.Width = windowSettings.Width; mainWindow.Height = windowSettings.Height; } else { mainWindow.WindowStartupLocation = WindowStartupLocation.CenterScreen; } VisualRoot = mainWindow; StorageProvider = mainWindow.StorageProvider; Clipboard = mainWindow.Clipboard ?? throw new NullReferenceException("Clipboard is null"); DesktopLifetime.MainWindow = mainWindow; DesktopLifetime.Exit += OnExit; DesktopLifetime.ShutdownRequested += OnShutdownRequested; } private static void ConfigureServiceProvider() { var services = ConfigureServices(); BeforeBuildServiceProvider?.Invoke(null, services); Services = services.BuildServiceProvider(); var settingsManager = Services.GetRequiredService(); if (Program.Args.DataDirectoryOverride is not null) { settingsManager.SetLibraryDirOverride(Program.Args.DataDirectoryOverride); } if (settingsManager.TryFindLibrary()) { Cultures.SetSupportedCultureOrDefault(settingsManager.Settings.Language); } else { Cultures.TrySetSupportedCulture(Settings.GetDefaultCulture()); } Services.GetRequiredService().StartEventListener(); } internal static void ConfigurePageViewModels(IServiceCollection services) { services.AddSingleton( provider => new MainWindowViewModel( provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService>(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService>() ) { Pages = { provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService() }, FooterPages = { provider.GetRequiredService() } } ); // Register disposable view models for shutdown cleanup services.AddSingleton(p => p.GetRequiredService()); } internal static void ConfigureDialogViewModels(IServiceCollection services, Type[] exportedTypes) { // Dialog factory services.AddSingleton>(provider => { var serviceManager = new ServiceManager(); var serviceManagedTypes = exportedTypes .Select( t => new { t, attributes = t.GetCustomAttributes(typeof(ManagedServiceAttribute), true) } ) .Where(t1 => t1.attributes is { Length: > 0 }) .Select(t1 => t1.t) .ToList(); foreach (var type in serviceManagedTypes) { ViewModelBase? GetInstance() => provider.GetRequiredService(type) as ViewModelBase; serviceManager.Register(type, GetInstance); } return serviceManager; }); } internal static IServiceCollection ConfigureServices() { var services = new ServiceCollection(); services.AddMemoryCache(); services.AddLazyInstance(); services.AddMessagePipe(); services.AddMessagePipeNamedPipeInterprocess("StabilityMatrix"); var exportedTypes = AppDomain .CurrentDomain.GetAssemblies() .Where(a => a.FullName?.StartsWith("StabilityMatrix") == true) .SelectMany(a => a.GetExportedTypes()) .ToArray(); var transientTypes = exportedTypes .Select(t => new { t, attributes = t.GetCustomAttributes(typeof(TransientAttribute), false) }) .Where( t1 => t1.attributes is { Length: > 0 } && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase) ) .Select(t1 => new { Type = t1.t, Attribute = (TransientAttribute)t1.attributes[0] }); foreach (var typePair in transientTypes) { if (typePair.Attribute.InterfaceType is null) { services.AddTransient(typePair.Type); } else { services.AddTransient(typePair.Attribute.InterfaceType, typePair.Type); } } var singletonTypes = exportedTypes .Select(t => new { t, attributes = t.GetCustomAttributes(typeof(SingletonAttribute), false) }) .Where( t1 => t1.attributes is { Length: > 0 } && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase) ) .Select( t1 => new { Type = t1.t, Attributes = t1.attributes.Cast().ToArray() } ); foreach (var typePair in singletonTypes) { foreach (var attribute in typePair.Attributes) { if (attribute.InterfaceType is null) { services.AddSingleton(typePair.Type); } else if (attribute.ImplType is not null) { services.AddSingleton(attribute.InterfaceType, attribute.ImplType); } else { services.AddSingleton(attribute.InterfaceType, typePair.Type); } } } ConfigurePageViewModels(services); ConfigureDialogViewModels(services, exportedTypes); // Other services services.AddSingleton(); services.AddSingleton( provider => (IDisposable)provider.GetRequiredService() ); // Rich presence services.AddSingleton(); services.AddSingleton( provider => provider.GetRequiredService() ); Config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .Build(); services.Configure(Config.GetSection(nameof(DebugOptions))); if (Compat.IsWindows) { services.AddSingleton(); } else if (Compat.IsLinux || Compat.IsMacOS) { services.AddSingleton(); } if (!Design.IsDesignMode) { services.AddSingleton(); services.AddSingleton(p => p.GetRequiredService()); } services.AddTransient(_ => { var client = new GitHubClient(new ProductHeaderValue("StabilityMatrix")); // var githubApiKey = Config["GithubApiKey"]; // if (string.IsNullOrWhiteSpace(githubApiKey)) // return client; // // client.Credentials = // new Credentials(""); return client; }); // Configure Refit and Polly var jsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; jsonSerializerOptions.Converters.Add(new ObjectToInferredTypesConverter()); jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter()); jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter()); jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter()); jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; var defaultRefitSettings = new RefitSettings { ContentSerializer = new SystemTextJsonContentSerializer(jsonSerializerOptions) }; // Refit settings for IApiFactory var defaultSystemTextJsonSettings = SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions(); defaultSystemTextJsonSettings.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; var apiFactoryRefitSettings = new RefitSettings { ContentSerializer = new SystemTextJsonContentSerializer(defaultSystemTextJsonSettings), }; // HTTP Policies var retryStatusCodes = new[] { HttpStatusCode.RequestTimeout, // 408 HttpStatusCode.InternalServerError, // 500 HttpStatusCode.BadGateway, // 502 HttpStatusCode.ServiceUnavailable, // 503 HttpStatusCode.GatewayTimeout // 504 }; var delay = Backoff.DecorrelatedJitterBackoffV2( medianFirstRetryDelay: TimeSpan.FromMilliseconds(80), retryCount: 5 ); var retryPolicy = HttpPolicyExtensions .HandleTransientHttpError() .Or() .OrResult(r => retryStatusCodes.Contains(r.StatusCode)) .WaitAndRetryAsync(delay); // Shorter timeout for local requests var localTimeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(3)); var localDelay = Backoff.DecorrelatedJitterBackoffV2( medianFirstRetryDelay: TimeSpan.FromMilliseconds(50), retryCount: 3 ); var localRetryPolicy = HttpPolicyExtensions .HandleTransientHttpError() .Or() .OrResult(r => retryStatusCodes.Contains(r.StatusCode)) .WaitAndRetryAsync( localDelay, onRetryAsync: (_, _) => { Debug.WriteLine("Retrying local request..."); return Task.CompletedTask; } ); // named client for update services.AddHttpClient("UpdateClient").AddPolicyHandler(retryPolicy); // Add Refit clients services .AddRefitClient(defaultRefitSettings) .ConfigureHttpClient(c => { c.BaseAddress = new Uri("https://civitai.com"); c.Timeout = TimeSpan.FromSeconds(30); }) .AddPolicyHandler(retryPolicy); services .AddRefitClient(defaultRefitSettings) .ConfigureHttpClient(c => { c.BaseAddress = new Uri("https://civitai.com"); c.Timeout = TimeSpan.FromSeconds(30); }) .AddPolicyHandler(retryPolicy); services .AddRefitClient(defaultRefitSettings) .ConfigureHttpClient(c => { c.BaseAddress = new Uri("https://auth.lykos.ai"); c.Timeout = TimeSpan.FromSeconds(60); }) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false }) .AddPolicyHandler(retryPolicy) .AddHttpMessageHandler( serviceProvider => new TokenAuthHeaderHandler(serviceProvider.GetRequiredService()) ); services .AddRefitClient(defaultRefitSettings) .ConfigureHttpClient(c => { c.BaseAddress = new Uri("https://openart.ai/api/public/workflows"); c.Timeout = TimeSpan.FromSeconds(30); }) .AddPolicyHandler(retryPolicy); // Add Refit client managers services.AddHttpClient("A3Client").AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy)); services .AddHttpClient("DontFollowRedirects") .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false }) .AddPolicyHandler(retryPolicy); /*services.AddHttpClient("IComfyApi") .AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy));*/ // Add Refit client factory services.AddSingleton( provider => new ApiFactory(provider.GetRequiredService()) { RefitSettings = apiFactoryRefitSettings, } ); ConditionalAddLogViewer(services); // Add logging services.AddLogging(builder => { builder.ClearProviders(); builder .AddFilter("Microsoft.Extensions.Http", LogLevel.Warning) .AddFilter("Microsoft.Extensions.Http.DefaultHttpClientFactory", LogLevel.Warning) .AddFilter("Microsoft", LogLevel.Warning) .AddFilter("System", LogLevel.Warning); builder.SetMinimumLevel(LogLevel.Debug); #if DEBUG builder.AddNLog( ConfigureLogging(), new NLogProviderOptions { IgnoreEmptyEventId = false, CaptureEventId = EventIdCaptureType.Legacy } ); #else builder.AddNLog(ConfigureLogging()); #endif }); return services; } /// /// Requests shutdown of the Current Application. /// /// This returns asynchronously *without waiting* for Shutdown /// Exit code for the application. /// If Application.Current is null public static void Shutdown(int exitCode = 0) { if (Current is null) throw new NullReferenceException("Current Application was null when Shutdown called"); if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) { try { var result = lifetime.TryShutdown(exitCode); Debug.WriteLine($"Shutdown: {result}"); if (result) { Environment.Exit(exitCode); } } catch (InvalidOperationException) { // Ignore in case already shutting down } } else { Environment.Exit(exitCode); } } private static void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e) { Debug.WriteLine("Start OnShutdownRequested"); if (e.Cancel) return; // Check if we need to dispose IAsyncDisposables if ( isAsyncDisposeComplete || Services.GetServices().ToList() is not { Count: > 0 } asyncDisposables ) return; // Cancel shutdown for now e.Cancel = true; isAsyncDisposeComplete = true; Debug.WriteLine("OnShutdownRequested Canceled: Disposing IAsyncDisposables"); Dispatcher .UIThread.InvokeAsync(async () => { foreach (var disposable in asyncDisposables) { Debug.WriteLine($"Disposing IAsyncDisposable ({disposable.GetType().Name})"); try { await disposable.DisposeAsync().ConfigureAwait(false); } catch (Exception ex) { Debug.Fail(ex.ToString()); } } }) .ContinueWith(_ => { // Shutdown again Debug.WriteLine("Finished disposing IAsyncDisposables, shutting down"); if (Dispatcher.UIThread.SupportsRunLoops) { Dispatcher.UIThread.Invoke(() => Shutdown()); } Environment.Exit(0); }) .SafeFireAndForget(); } private static void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs args) { Debug.WriteLine("Start OnExit"); // Services.GetRequiredService().OnShutdown(); var settingsManager = Services.GetRequiredService(); // If RemoveFolderLinksOnShutdown is set, delete all package junctions if (settingsManager is { IsLibraryDirSet: true, Settings.RemoveFolderLinksOnShutdown: true }) { var sharedFolders = Services.GetRequiredService(); sharedFolders.RemoveLinksForAllPackages(); } Debug.WriteLine("Start OnExit: Disposing services"); // Dispose IDisposable services foreach (var disposable in Services.GetServices()) { Debug.WriteLine($"Disposing {disposable.GetType().Name}"); disposable.Dispose(); } Debug.WriteLine("End OnExit"); } private static LoggingConfiguration ConfigureLogging() { var setupBuilder = LogManager.Setup(); ConditionalAddLogViewerNLog(setupBuilder); setupBuilder.LoadConfiguration(builder => { var debugTarget = builder .ForTarget("console") .WriteTo( new DebuggerTarget { Layout = "[${level:format=TriLetter}] " + "${callsite:includeNamespace=false:captureStackTrace=false}: ${message}" } ) .WithAsync(); var fileTarget = builder .ForTarget("logfile") .WriteTo( new FileTarget { Layout = "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}", ArchiveOldFileOnStartup = true, FileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log", ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log", ArchiveNumbering = ArchiveNumberingMode.Rolling, MaxArchiveFiles = 2 } ) .WithAsync(); // Filter some sources to be warn levels or above only builder.ForLogger("System.*").WriteToNil(NLog.LogLevel.Warn); builder.ForLogger("Microsoft.*").WriteToNil(NLog.LogLevel.Warn); builder.ForLogger("Microsoft.Extensions.Http.*").WriteToNil(NLog.LogLevel.Warn); // Disable console trace logging by default builder .ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel") .WriteToNil(NLog.LogLevel.Debug); // Disable LoadableViewModelBase trace logging by default builder .ForLogger("StabilityMatrix.Avalonia.ViewModels.Base.LoadableViewModelBase") .WriteToNil(NLog.LogLevel.Debug); builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(debugTarget); builder.ForLogger().FilterMinLevel(NLog.LogLevel.Debug).WriteTo(fileTarget); #if DEBUG var logViewerTarget = builder .ForTarget("DataStoreLogger") .WriteTo(new DataStoreLoggerTarget() { Layout = "${message}" }); builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(logViewerTarget); #endif }); // Sentry if (SentrySdk.IsEnabled) { LogManager.Configuration.AddSentry(o => { o.InitializeSdk = false; o.Layout = "${message}"; o.ShutdownTimeoutSeconds = 5; o.IncludeEventDataOnBreadcrumbs = true; o.BreadcrumbLayout = "${logger}: ${message}"; // Debug and higher are stored as breadcrumbs (default is Info) o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug; // Error and higher is sent as event (default is Error) o.MinimumEventLevel = NLog.LogLevel.Error; }); } LogManager.ReconfigExistingLoggers(); return LogManager.Configuration; } /// /// Opens a dialog to save the current view as a screenshot. /// /// Only available in debug builds. [Conditional("DEBUG")] internal static void DebugSaveScreenshot(int dpi = 96) { const int scale = 2; dpi *= scale; var results = new List(); var targets = new List { VisualRoot }; foreach (var visual in targets.Where(x => x != null)) { var rect = new Rect(visual!.Bounds.Size); var pixelSize = new PixelSize((int)rect.Width * scale, (int)rect.Height * scale); var dpiVector = new Vector(dpi, dpi); var ms = new MemoryStream(); using (var bitmap = new RenderTargetBitmap(pixelSize, dpiVector)) { bitmap.Render(visual); bitmap.Save(ms); } results.Add(ms); } Dispatcher.UIThread.InvokeAsync(async () => { var dest = await StorageProvider.SaveFilePickerAsync( new FilePickerSaveOptions() { SuggestedFileName = "screenshot.png", ShowOverwritePrompt = true } ); if (dest?.TryGetLocalPath() is { } localPath) { var localFile = new FilePath(localPath); foreach (var (i, stream) in results.Enumerate()) { var name = localFile.NameWithoutExtension; if (results.Count > 1) { name += $"_{i + 1}"; } localFile = localFile.Directory!.JoinFile(name + ".png"); localFile.Create(); await using var fileStream = localFile.Info.OpenWrite(); stream.Seek(0, SeekOrigin.Begin); await stream.CopyToAsync(fileStream); } } }); } [Conditional("DEBUG")] private static void ConditionalAddLogViewer(IServiceCollection services) { #if DEBUG services.AddLogViewer(); #endif } [Conditional("DEBUG")] private static void ConditionalAddLogViewerNLog(ISetupBuilder setupBuilder) { #if DEBUG setupBuilder.SetupExtensions( extensionBuilder => extensionBuilder.RegisterTarget("DataStoreLogger") ); #endif } }