I use Autofac
a lot and for everything and when you make a mistake and forget to register a dependency etc. it'll tell you exaclty what's wrong. Although its exceptions are very helpful, the exception strings are at the same time hard to read because it's a large blob of text:
System.Exception: Blub ---> Autofac.Core.DependencyResolutionException: An error occurred during the activation of a particular registration. See the inner exception for details. Registration: Activator = User (ReflectionActivator), Services = [UserQuery+User], Lifetime = Autofac.Core.Lifetime.CurrentScopeLifetime, Sharing = None, Ownership = OwnedByLifetimeScope ---> None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'UserQuery+User' can be invoked with the available services and parameters: Cannot resolve parameter 'System.String name' of constructor 'Void .ctor(System.String)'. (See inner exception for details.) ---> Autofac.Core.DependencyResolutionException: None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'UserQuery+User' can be invoked with the available services and parameters: Cannot resolve parameter 'System.String name' of constructor 'Void .ctor(System.String)'. at Autofac.Core.Activators.Reflection.ReflectionActivator.GetValidConstructorBindings(IComponentContext context, IEnumerable`1 parameters)
To find the reason for this exception in such a string isn't easy. This is better done by some tool so I created. It reads the string for me and presents it in a more friendly way. I implemented it as a Debugger Visualizer.
DebuggerVisualizer
The ExceptionVisualizer
is virtually a single function that shows the WPF
window with the exception strings:
public class ExceptionVisualizer : DialogDebuggerVisualizer { protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider) { var data = (IEnumerable<ExceptionInfo>)objectProvider.GetObject(); var window = new Window { Title = "Exception Visualizer", Width = SystemParameters.WorkArea.Width * 0.4, Height = SystemParameters.WorkArea.Height * 0.6, Content = new DebuggerVisualizers.ExceptionControl { DataContext = new ExceptionControlModel { Exceptions = data }, HorizontalAlignment = HorizontalAlignment.Stretch }, WindowStartupLocation = WindowStartupLocation.CenterScreen }; window.ShowDialog(); } public static void TestShowVisualizer(object objectToVisualize) { var visualizerHost = new VisualizerDevelopmentHost(objectToVisualize, typeof(ExceptionVisualizer)); visualizerHost.ShowVisualizer(); } }
It receives a collection of ExceptionInfo
s that I create with these helpers that parse the string by removing the stack trace and extracting exception names and messages. I know I could use the Exception
object to extract exception names and messages but I'm going to reuse this parser in another tools later for parsing and searching logs so I didn't want to have two solutions.
public class ExceptionParser { public static string RemoveStackStrace(string exceptionString) { // Stack-trace begins at the first 'at' return Regex.Split(exceptionString, @"^\s{3}at", RegexOptions.Multiline).First(); } public static IEnumerable<ExceptionInfo> ParseExceptions(string exceptionString) { // Exceptions start with 'xException:' string and end either with '$' or '--->' if an inner exception follows. return Regex .Matches(exceptionString, @"(?<exception>(^|\w+)?Exception):\s(?<message>(.|\n)+?)(?=( --->|$))", RegexOptions.ExplicitCapture) .Cast<Match>() .Select(m => new ExceptionInfo { Name = m.Groups["exception"].Value, Message = m.Groups["message"].Value }); } } public static class EnumerableExtensions { public static IEnumerable<T> Reverse<T>(this IEnumerable<T> source) => new Stack<T>(source); }
The DTO is
[Serializable] public class ExceptionInfo { public string Name { get; set; } public string Message { get; set; } public override string ToString() { return Name + Environment.NewLine + Message; } }
GUI
On the UI side there is simple a WPF.UserControl
with a ListBox
and two TextBox
es. The Close
button closes the window and the Copy
button copies the list to Clipboard
and contains a small animation that lets the button shrink and grow back to its original size. In order to keep it in the middle I also animate the right Margin
.
<UserControl x:Class="Reusable.Apps.ExceptionControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Reusable.Apps" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" Background="#FF404040" > <UserControl.Resources> <local:ExceptionControlModel x:Key="DesignViewModel" /> <Style TargetType="TextBlock" x:Key="NameStyle"> <Setter Property="FontSize" Value="20"/> <Setter Property="FontWeight" Value="Bold"/> <Setter Property="FontFamily" Value="Consolas"/> <Setter Property="Foreground" Value="DarkOrange"/> <Setter Property="Margin" Value="0,10,0,0" /> </Style> <Style TargetType="TextBlock" x:Key="MessageStyle"> <Setter Property="FontSize" Value="16"/> <Setter Property="FontFamily" Value="Segoe UI"/> <Setter Property="TextWrapping" Value="Wrap"/> <Setter Property="Margin" Value="0,5,0,0" /> <Setter Property="Foreground" Value="WhiteSmoke"/> </Style> <Style x:Key="Theme" TargetType="{x:Type Control}"> <Setter Property="Background" Value="#FF404040"></Setter> </Style> </UserControl.Resources> <UserControl.CommandBindings> <CommandBinding Command="Close"></CommandBinding> </UserControl.CommandBindings> <Grid > <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <ListBox ItemsSource="{Binding Exceptions}" d:DataContext="{Binding Source={StaticResource DesignViewModel}}" Style="{StaticResource Theme}" Grid.Row="0" BorderThickness="0"> <ListBox.ItemContainerStyle> <Style TargetType="ListBoxItem"> <Setter Property="Width" Value="{Binding (Grid.ActualWidth), RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Grid}}}" /> </Style> </ListBox.ItemContainerStyle> <ListBox.ItemTemplate> <DataTemplate> <StackPanel> <Border> <TextBlock Text="{Binding Name}" Style="{StaticResource NameStyle}" /> </Border> <TextBlock Text="{Binding Message}" Style="{StaticResource MessageStyle}" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <DockPanel Grid.Row="1" HorizontalAlignment="Right" > <DockPanel.Resources> <Style TargetType="Button"> <Setter Property="Margin" Value="0,5,10,5" /> <Setter Property="Width" Value="100"/> <Setter Property="Height" Value="25"></Setter> <Setter Property="FontSize" Value="15"/> </Style> </DockPanel.Resources> <Button Content="Copy" Command="{x:Static local:ExceptionControlModel.CopyCommand}" CommandParameter="{Binding}"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="Width" From="100" To="90" Duration="0:0:0.25"/> <DoubleAnimation Storyboard.TargetProperty="Width" From="90" To="100" Duration="0:0:0.25"/> </Storyboard> </BeginStoryboard> <BeginStoryboard> <Storyboard> <ThicknessAnimation Storyboard.TargetProperty="Margin" From="0,5,10,5" To="0,5,15,5" Duration="0:0:0.25"/> <ThicknessAnimation Storyboard.TargetProperty="Margin" From="0,5,15,5" To="0,5,10,5" Duration="0:0:0.25"/> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Button.Triggers> </Button> <Button Content="Close" Command="{x:Static local:ExceptionControlModel.CloseCommand}" CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" /> </DockPanel> </Grid> </UserControl>
and this is its model with some design-time data:
public class ExceptionControlModel { public static readonly ICommand CloseCommand = CommandFactory<Window>.Create(p => p.Close()); public static readonly ICommand CopyCommand = CommandFactory<ExceptionControlModel>.Create(p => p.CopyToClipboard()); public IEnumerable<ExceptionInfo> Exceptions { get; set; } = new[] { // This is design-time data. new ExceptionInfo {Name = "DependencyResolutionException", Message = "An error occurred during the activation of a particular registration. See the inner exception for details. Registration: Activator = User (ReflectionActivator), Services = [UserQuery+User], Lifetime = Autofac.Core.Lifetime.CurrentScopeLifetime, Sharing = None, Ownership = OwnedByLifetimeScope"}, new ExceptionInfo {Name = "DependencyResolutionException", Message = "None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'UserQuery+User' can be invoked with the available services and parameters: Cannot resolve parameter 'System.String name' of constructor 'Void .ctor(System.String)'."}, }; private void CopyToClipboard() { Clipboard.SetText(Exceptions.Join(Environment.NewLine + Environment.NewLine)); } }
The command creation is supported with this helper factory that passes a strong type to the handler delegate:
public static class CommandFactory<T> { public static ICommand Create([NotNull] Action<T> execute) { if (execute == null) throw new ArgumentNullException(nameof(execute)); return new Command(parameter => execute((T)parameter)); } public static ICommand Create([NotNull] Action<T> execute, [NotNull] Predicate<object> canExecute) { if (execute == null) throw new ArgumentNullException(nameof(execute)); if (canExecute == null) throw new ArgumentNullException(nameof(canExecute)); return new Command(parameter => execute((T)parameter), parameter => canExecute((T)parameter)); } private class Command : ICommand { private readonly Action<object> _execute; private readonly Predicate<object> _canExecute; public Command(Action<object> execute) : this(execute, _ => true) { } public Command(Action<object> execute, Predicate<object> canExecute) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } #region ICommand public event EventHandler CanExecuteChanged { add => CommandManager.RequerySuggested += value; remove => CommandManager.RequerySuggested -= value; } [DebuggerStepThrough] public bool CanExecute(object parameter) => _canExecute(parameter); [DebuggerStepThrough] public void Execute(object parameter) => _execute(parameter); #endregion } }
Example
I use the following code for testing where I try to resolve an instance of the User
class which is missing a dependency:
internal class ExceptionVisualizerExperiment { public static void Run() { try { try { var builder = new ContainerBuilder(); builder.RegisterType<User>(); var container = builder.Build(); container.Resolve<User>(); throw new DivideByZeroException("Blub"); } catch (Exception ex) { throw new Exception("Blub", ex); } } catch (Exception ex) { var exceptionString = ex.ToString(); exceptionString = ExceptionParser.RemoveStackStrace(exceptionString); var exceptions = ExceptionParser.ParseExceptions(exceptionString); ExceptionVisualizer .TestShowVisualizer(EnumerableExtensions.Reverse(exceptions)); } } public static void TestShowVisualizer(object objectToVisualize) { var visualizerHost = new VisualizerDevelopmentHost(objectToVisualize, typeof(ExceptionVisualizer)); visualizerHost.ShowVisualizer(); } } internal class User { public User(string name) { } }
Running
There is one more component that is required to run it in Visual Studio. It's the custom object-source for the exception serialization:
public class ExceptionVisualizerObjectSource : VisualizerObjectSource { public override void GetData(object target, Stream outgoingData) { var exceptionString = target.ToString(); exceptionString = ExceptionParser.RemoveStackStrace(exceptionString); var exceptions = ExceptionParser.ParseExceptions(exceptionString).Reverse(); Serialize(outgoingData, exceptions); } }
This needs to be registered with:
[assembly: DebuggerVisualizer( visualizer: typeof(ExceptionVisualizer), visualizerObjectSource: typeof(ExceptionVisualizerObjectSource), Target = typeof(Exception), Description = "Exception Visualizer")]
So what do you think about the parsing of the excpetion string and the UI? It's my first WPF application for a long time so it's probably not the state of the art. Is there anything you would improve either in the back and or front end?
As always, you can also find it on my GitHub under Reusable.DebuggerVisualizers. The Console
code is here.
.InnerException
? Are they coming from a log file?\$\endgroup\$