Imported from Mythetech/Mythetech.Framework (
AGENTS.md). Install upstream withnpx skills add Mythetech/Mythetech.Framework. Copyright stays with the author.
Mythetech.Framework Coding Patterns
Note: This framework is built on MudBlazor. For advanced Blazor component patterns, see MudBlazor's AGENTS.md which covers ParameterState patterns, EventCallback best practices, and bUnit testing patterns.
Table of Contents
Async Patterns
Never Use async void
Never mark a method async void - this swallows exceptions and makes debugging impossible.
Bad:
private async void OnStateChanged(object? sender, EventArgs e)
{
await InvokeAsync(StateHasChanged);
}
Good (for async work with side effects - use MessageBus):
// Dispatch an event that can be handled by an async Task consumer
await MessageBus.PublishAsync(new SomeStateChangedMessage());
Never Fire-and-Forget Async Calls
Never call an async method without awaiting it. This includes patterns like _ = SomeAsync().
Bad:
protected override void OnInitialized()
{
_ = SomeService.InitializeAsync(); // Fire and forget - causes bugs
}
Good:
protected override async Task OnInitializedAsync()
{
await SomeService.InitializeAsync();
}
Never Call Async from Property Setters
Property setters cannot be async, so calling async methods from them requires fire-and-forget which violates the rules above.
Bad:
public string Query
{
get => _query;
set
{
_query = value;
_ = SearchAsync(); // Fire-and-forget in setter - banned
}
}
Good:
// Use event callbacks from components
<MudTextField @bind-Value="_query" ValueChanged="@OnQueryChangedAsync" />
@code {
private async Task OnQueryChangedAsync(string value)
{
_query = value;
await SearchAsync();
}
}
Blazor Component Patterns
Event Handler Subscriptions
When subscribing to C# events (like StateChanged) from components:
- Subscribe in
OnInitialized() - Unsubscribe in
Dispose() - For complex async work, dispatch via
IMessageBusinstead
@implements IDisposable
@code {
[Inject] private SomeState State { get; set; } = default!;
protected override void OnInitialized()
{
State.StateChanged += OnStateChanged;
}
private void OnStateChanged(object? sender, EventArgs e)
{
Console.WriteLine("State Changed");
StateHasChanged();
}
public void Dispose()
{
State.StateChanged -= OnStateChanged;
}
}
Cross-Platform Link Opening
Adding New Components
When adding new components to the framework:
- Create the component in
Mythetech.Framework/Components/[ComponentName]/ - Add a storybook in
Mythetech.Framework.Storybook/Stories/[ComponentName].stories.razor- Storybooks help visually test components in isolation
- Include multiple stories showing different states and configurations
- See existing stories (e.g.,
Button.stories.razor,Badge.stories.razor) for patterns
- Add unit tests if the component has logic that should be tested
Use ILinkOpenService for opening external URLs, never JS interop.
Since the framework supports desktop apps (Hermes), window.open() via JS interop doesn't work.
@using Mythetech.Framework.Infrastructure
[Inject] protected ILinkOpenService LinkService { get; set; } = default!;
private async Task OpenLink()
{
await LinkService.OpenLinkAsync("https://example.com");
}
State Management
State Classes Own Their Data
Never manipulate state from a component and then call a public Notify method. State classes own their data and mutations.
Bad:
// In Component.razor
private void UpdateItems()
{
var items = State.GetItems().ToList();
items.Add(newItem);
State.NotifyChanged(); // Public notify = code smell
}
Good:
// In Component.razor
private void UpdateItems()
{
State.AddItem(newItem); // Request change from state
}
// In SomeState.cs
public void AddItem(Item item)
{
_items.Add(item);
NotifyStateChanged(); // Private notify
}
private void NotifyStateChanged() => StateChanged?.Invoke(this, EventArgs.Empty);
Event Naming Convention
Use {Subject}Changed pattern for events. Avoid On prefix.
Bad:
public event EventHandler? OnChange;
public event EventHandler? OnStateChanged;
Good:
public event EventHandler? StateChanged;
public event EventHandler<bool>? EnabledChanged;
Testing Patterns
Component Testing with bUnit
Use bUnit for component testing with MudBlazor services:
public class MyComponentTests : TestContext
{
public MyComponentTests()
{
Services.AddMudServices();
JSInterop.Mode = JSRuntimeMode.Loose;
}
[Fact]
public void Component_RendersCorrectly()
{
var cut = RenderComponent<MyComponent>(parameters => parameters
.Add(p => p.Text, "Hello"));
cut.Markup.ShouldContain("Hello");
}
}
Mocking Services
Use NSubstitute for interface dependencies:
var linkService = Substitute.For<ILinkOpenService>();
Services.AddSingleton(linkService);
// ... render component and trigger action ...
await linkService.Received(1).OpenLinkAsync("https://example.com");
Async Event Testing
For mouse events, use TriggerEventAsync with the correct event name:
[Fact]
public async Task Component_HandlesMouseEnter()
{
var cut = RenderComponent<HoverStack>(/* ... */);
var container = cut.Find("div");
await container.TriggerEventAsync("onmouseenter", new MouseEventArgs());
cut.Markup.ShouldContain("Hovering: True");
}
External References
MudBlazor AGENTS.md
This framework is built on MudBlazor. For complex Blazor component patterns, consult MudBlazor's AGENTS.md which contains valuable patterns for:
- ParameterState pattern for handling async work triggered by parameter changes
- EventCallback invocation best practices
- bUnit testing patterns for MudBlazor components
- Why
_ = SomeAsync()is treated as an error (CS4014)
MudBlazor utilities and base classes are available for use in this framework.
Project Structure
- Mythetech.Framework - Core component library with UI components and infrastructure
- Mythetech.Framework.Desktop - Desktop-specific implementations (Hermes, LiteDB)
- Mythetech.Framework.WebAssembly - Browser-specific implementations
- Mythetech.Framework.Storybook - Component documentation with BlazingStory
- Mythetech.Framework.Test - Unit tests with xUnit, bUnit, NSubstitute, Shouldly