dotnet-expert

.NET 8.0 development expertise for VoiceLite including WPF patterns, async/await best practices, IDisposable patterns, and NuGet package recommendations. Activates when working on C# code, WPF XAML, or .NET-specific issues.

$ Instalar

git clone https://github.com/mikha08-rgb/VoiceLite /tmp/VoiceLite && cp -r /tmp/VoiceLite/.claude/skills/dotnet-expert ~/.claude/skills/VoiceLite

// tip: Run this command in your terminal to install the skill


name: dotnet-expert description: .NET 8.0 development expertise for VoiceLite including WPF patterns, async/await best practices, IDisposable patterns, and NuGet package recommendations. Activates when working on C# code, WPF XAML, or .NET-specific issues.

.NET Expert

Specialized .NET 8.0 development guidance for VoiceLite's WPF desktop application.

When This Skill Activates

  • Working on .cs or .xaml files
  • Mentions: "NuGet", "WPF", "XAML", "IDisposable", "async/await", "Dispatcher"
  • Compiler errors: CS#### error codes
  • Performance issues: Memory leaks, GC pressure, threading
  • .NET-specific questions: "How do I..." in C# context

VoiceLite-Specific .NET Patterns

1. Disposal Pattern (CRITICAL for Audio Resources)

NAudio WaveInEvent MUST be disposed to prevent memory leaks:

// AudioRecorder.cs - Proper disposal pattern
public class AudioRecorder : IDisposable
{
    private WaveInEvent? _waveIn;
    private bool _disposed = false;

    public void Dispose()
    {
        if (_disposed) return;

        // Stop recording first
        _waveIn?.StopRecording();

        // Dispose NAudio resources
        _waveIn?.Dispose();
        _waveIn = null;

        _disposed = true;
        GC.SuppressFinalize(this);
    }

    ~AudioRecorder()
    {
        Dispose();
    }
}

// CRITICAL: Test coverage required
// See: VoiceLite.Tests/Services/DisposalTests.cs

2. Dispatcher Pattern (UI Thread Safety)

NEVER update UI from background thread - always use Dispatcher:

// MainWindow.xaml.cs - Correct UI updates
private async void OnTranscriptionCompleted(object sender, TranscriptionEventArgs e)
{
    // This event may fire from background thread!

    // ✅ CORRECT: Use Dispatcher for UI updates
    Application.Current.Dispatcher.Invoke(() =>
    {
        StatusLabel.Content = "Transcription complete";
        TranscriptionText.Text = e.Text;
    });

    // ❌ WRONG: Direct UI access from background thread
    // StatusLabel.Content = "..."; // Will throw InvalidOperationException!
}

// Async version
await Application.Current.Dispatcher.InvokeAsync(() =>
{
    // UI updates here
});

3. Async Event Handlers

Event handlers can be async void (only place this is acceptable):

// MainWindow.xaml.cs
private async void OnAudioFileReady(object sender, AudioFileReadyEventArgs e)
{
    // async void is OK for event handlers
    try
    {
        await _whisperService.TranscribeAsync(e.FilePath);
    }
    catch (Exception ex)
    {
        ErrorLogger.Log($"Transcription failed: {ex.Message}");
    }
}

// Why async void here?
// - Event handler signature requires void return
// - UI remains responsive during await
// - Exceptions caught locally (critical!)

4. Process Management

Whisper.exe subprocess management:

// PersistentWhisperService.cs - Proper process handling
public async Task<string> TranscribeAsync(string audioPath)
{
    Process? process = null;
    try
    {
        process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "whisper.exe",
                Arguments = $"-m {modelPath} -f {audioPath}",
                RedirectStandardOutput = true,
                UseShellExecute = false,
                CreateNoWindow = true
            }
        };

        process.Start();

        // Calculate timeout (3x audio length)
        var timeout = (int)(audioLength * 3000);

        if (!process.WaitForExit(timeout))
        {
            // Timeout - kill process
            process.Kill();
            throw new TimeoutException($"Whisper timeout after {timeout}ms");
        }

        return await process.StandardOutput.ReadToEndAsync();
    }
    finally
    {
        // ALWAYS dispose process
        process?.Dispose();
    }
}

NuGet Package Recommendations

VoiceLite's tech stack and recommended versions:

PackageVersionUse CaseNotes
NAudio2.2.1Audio recordingUse WaveInEvent for recording
H.InputSimulator1.2.1Text injectionKeyboard & clipboard simulation
Hardcodet.NotifyIcon.Wpf2.0.1System tray iconWPF-compatible tray support
System.Text.Json9.0.9Settings serializationPrefer over Newtonsoft.Json
System.Management8.0.0System infoUsed for version checking
Moq4.20+Unit testingMock dependencies in tests
FluentAssertions6.12+Test assertionsReadable test assertions
xUnit2.6+Test frameworkVoiceLite test framework

Avoid:

  • ❌ Newtonsoft.Json (use System.Text.Json for .NET 8.0)
  • ❌ Old NAudio versions (ensure 2.2.1+ for stability)

Common .NET 8.0 Issues & Solutions

Issue: System.Text.Json Doesn't Serialize Property

Symptom: Property missing from JSON output

Fix: Ensure property is public with getter/setter:

// ❌ WRONG: Private or missing setter
private string Model { get; set; }
public string Model { get; }

// ✅ CORRECT: Public with both getter and setter
public string Model { get; set; }

// Alternative: Use JsonPropertyName attribute
[JsonPropertyName("model")]
public string Model { get; init; } // init-only is OK

Issue: WPF Dispatcher Throws on Test Thread

Symptom: System.InvalidOperationException: The calling thread must be STA

Fix: Mock Dispatcher in tests or use SynchronizationContext:

// In tests: Mock Application.Current
var mockApp = new Mock<Application>();
// ... setup mock dispatcher

// Or: Use SynchronizationContext
SynchronizationContext.SetSynchronizationContext(
    new SynchronizationContext()
);

Issue: Memory Leak with Event Handlers

Symptom: Objects not garbage collected, memory grows

Fix: Always unsubscribe in Dispose():

public class AudioRecorder : IDisposable
{
    public AudioRecorder()
    {
        _waveIn.DataAvailable += OnDataAvailable;
    }

    public void Dispose()
    {
        // CRITICAL: Unsubscribe events
        _waveIn.DataAvailable -= OnDataAvailable;
        _waveIn?.Dispose();
    }
}

Issue: Async Deadlock in WPF

Symptom: UI freezes on await call

Fix: Use ConfigureAwait(false) for non-UI code:

// In service layer (non-UI)
public async Task<string> TranscribeAsync(string path)
{
    var result = await ProcessAudioAsync(path)
        .ConfigureAwait(false); // Don't capture UI context

    return result;
}

// In UI layer (keep UI context)
private async void OnButtonClick(object sender, RoutedEventArgs e)
{
    var result = await _service.TranscribeAsync(path);
    // No ConfigureAwait here - we NEED UI context for updates
    ResultLabel.Text = result;
}

WPF-Specific Patterns

XAML Data Binding

<!-- ModernStyles.xaml pattern -->
<Style x:Key="ModernButton" TargetType="Button">
    <Setter Property="Background" Value="#2D2D30"/>
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="BorderBrush" Value="#3E3E42"/>
    <Setter Property="Padding" Value="10,5"/>
</Style>

<!-- Usage in MainWindow.xaml -->
<Button Style="{StaticResource ModernButton}"
        Click="OnRecordClick"
        Content="Record"/>

Value Converters

// Converters/RelativeTimeConverter.cs
public class RelativeTimeConverter : IValueConverter
{
    public object Convert(object value, Type targetType,
                         object parameter, CultureInfo culture)
    {
        if (value is DateTime dt)
        {
            var diff = DateTime.Now - dt;
            if (diff.TotalMinutes < 1) return "Just now";
            if (diff.TotalHours < 1) return $"{(int)diff.TotalMinutes} mins ago";
            if (diff.TotalDays < 1) return $"{(int)diff.TotalHours} hours ago";
            return $"{(int)diff.TotalDays} days ago";
        }
        return value;
    }

    public object ConvertBack(object value, Type targetType,
                             object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

.NET 8.0 Performance Tips

1. Use Span for Buffer Operations

// ✅ GOOD: Use Span<byte> for audio buffers
public void ProcessAudioBuffer(Span<byte> buffer)
{
    // No allocations, stack-based
    for (int i = 0; i < buffer.Length; i++)
    {
        buffer[i] = 0; // Process in-place
    }
}

// ❌ BAD: Array allocation every time
public void ProcessAudioBuffer(byte[] buffer)
{
    var temp = new byte[buffer.Length]; // Heap allocation!
}

2. String Interpolation for Performance

// ✅ GOOD: String interpolation (compiled to string.Format)
var msg = $"Transcribed {count} files in {elapsed}ms";

// ❌ BAD: String concatenation (multiple allocations)
var msg = "Transcribed " + count + " files in " + elapsed + "ms";

3. Nullable Reference Types

// Enable in .csproj
<Nullable>enable</Nullable>

// Use in code
public class AudioRecorder
{
    private WaveInEvent? _waveIn; // Nullable
    private string _model = ""; // Non-nullable, initialized

    public string Model => _model; // Non-nullable guarantee
}

Testing Patterns

Unit Test Example

// VoiceLite.Tests/Services/WhisperServiceTests.cs
public class WhisperServiceTests
{
    [Fact]
    public async Task TranscribeAsync_ValidAudio_ReturnsText()
    {
        // Arrange
        var service = new PersistentWhisperService();
        var testAudio = "test_5s.wav";

        // Act
        var result = await service.TranscribeAsync(testAudio);

        // Assert
        result.Should().NotBeNullOrEmpty();
        result.Length.Should().BeGreaterThan(0);
    }

    [Fact]
    public void Dispose_Called_DisposesResources()
    {
        // Arrange
        var recorder = new AudioRecorder();

        // Act
        recorder.Dispose();

        // Assert
        // Verify no exceptions thrown
        // Check memory released (advanced)
    }
}

Compiler Error Quick Reference

Error CodeMeaningCommon Fix
CS0103Name does not existAdd using statement or fix typo
CS0161Not all code paths returnAdd return statement
CS1061No definition for methodCheck method name, add reference
CS8602Null reference warningAdd null check or ! operator
CS8618Non-nullable field not initializedInitialize in constructor or use = null!

Resources