| applyTo | ** |
|---|
Cross-platform remote access and control. .NET 10 backend (ASP.NET Core), Blazor WebAssembly frontend, Avalonia desktop apps.
- Build:
dotnet build ControlR.slnx --verbosity quiet(no output = success) - Run: Use IDE launch profiles — "Full Stack" in VS/Rider; "Full Stack (Debug)" or "Full Stack (Hot Reload)" in VS Code.
- Don't attempt to fix warning
BB0001: Member '{member_name}' is not in the correct order.
- Exclude
ControlR.Web.Server/novnc/and anynode_modules/directories from context.
Services use extension methods, not direct Program.cs registrations:
| Project | Method | File |
|---|---|---|
| ControlR.Agent | AddControlRAgent |
ControlR.Agent.Common\Startup\HostBuilderExtensions.cs |
| ControlR.Web.Server | AddControlrServer |
ControlR.Web.Server\Startup\WebApplicationBuilderExtensions.cs |
| ControlR.Web.Client | AddControlrWebClient |
ControlR.Web.Client\Startup\IServiceCollectionExtensions.cs |
| ControlR.DesktopClient | AddControlrDesktop |
ControlR.DesktopClient\StaticServiceProvider.cs |
- AgentHub — device heartbeats → forwarded to ViewerHub groups.
- ViewerHub — web client connections and remote control requests.
- Hub groups organized by tenant, device tags, and user roles via
HubGroupNames.GetTenantDevicesGroupName(),GetTagGroupName(), etc. - Agent ↔ DesktopClient IPC via named pipes (
IIpcConnection). Agent forwardsRemoteControlRequestIpcDtoto the user-session DesktopClient; DesktopClient reports back for relay to server.
DTOs go under \Libraries\ControlR.Libraries.Api.Contracts\Dtos\:
HubDtos/— SignalR hub payloadsIpcDtos/— Agent ↔ DesktopClient IPCServerApi/— REST APIRemoteControlDtos/— remote control, routed through websocket relay
- Platform implementations in
ControlR.Agent.CommonunderServices.Windows/,Services.Linux/,Services.Mac/. - Desktop client isolates native code in
ControlR.DesktopClient.Windows/,.Linux/,.Mac/with shared code inControlR.DesktopClient.Common. - Conditional compilation symbols:
IS_WINDOWS,IS_MACOS,IS_LINUX,IS_UNIX(defined inDirectory.Build.props). - Use
[SupportedOSPlatform]for platform-specific code. - Platform detection via
ISystemEnvironment.PlatformandRuntimeInformation. - macOS debug builds: disable app-bundle output; emit managed launch files (
.dll,.deps.json,.runtimeconfig.json) so VS Code can launch viadotnet.
- Use 2 spaces for indentation.
- Braces go on new lines.
- Prefix private fields (including static) with
_and use camelCase. E.g.private readonly IFileSystem _fileSystem; - Constants:
PascalCasewithconstmodifier. E.g.private const int MaxRetries = 5; - Prefer var over explicit types.
- Example:
var directories = _fileSystem.GetDirectories(path);
- Example:
- Use collection expressions (
[]).- Example:
private readonly Dictionary<string, uint> _displayNodeIds = [];
- Example:
- No null-forgiving operator (
!) outside tests, except the following scenarios:- In tests, where a null value would result in a test failure anyway.
- Within EF Core queries that execute server-side.
- Blazor framework-injected properties ([SupplyParameterFromForm], [CascadingParameter]) that cannot have a property initializer.
- Null-forgiving examples:
- DON'T:
var result = myObject!.GetValue(); - DO:
var result = myObject?.GetValue() ?? throw new InvalidOperationException("myObject is not initialized."); - OK:
var properties = await _dbContext.Users.Select(x => x.SomeNavigation!.SomeProperty).ToListAsync(); - OK:
[CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
- DON'T:
- Use
requiredkeyword where applicable. - Use
usingstatements forIDisposableresources, andawait usingforIAsyncDisposable.- Example:
using var stream = new FileStream(path, FileMode.Open); - Example:
await using var connection = new DatabaseConnection();
- Example:
- No TODOs, placeholder code, or "in production you should..." comments. Every implementation must be complete.
- Don't add "Async" suffix on async methods unless distinguishing from a sync overload.
- Example:
public async Task Connect()if there's nopublic void Connect(). - Example: If
public void Connect()exists, thenpublic async Task ConnectAsync().
- Example:
- Put public types in their own class file, with the below exceptions.
- If an interface has only one implementation, those types can go in the same file. E.g.
ISecretProviderandSecretProvidercan both go inSecretProvider.cs. - Enums that are tightly coupled to another class and only used there.
- If an interface has only one implementation, those types can go in the same file. E.g.
- Reduce indentation by returning/continuing early and inverting conditions when appropriate.
- Constructor parameter order: put concrete classes/implementations before interfaces.
- Component-scoped JS/CSS:
MyComponent.razor.jsandMyComponent.razor.cssalongsideMyComponent.razor. - JS interop: inherit
JsInteropableComponentin both.razorand code-behind.csfiles. - Prefer code-behind
.razor.csfiles for C# logic. Check for existing code-behind before editing.razorfiles. - Use MudBlazor components.
- Avalonia UI with MVVM pattern.
- All UI text bound via
x:Static common:Localization.KeyName. Add keys to JSON files under/ControlR.DesktopClient.Common/Resources/Strings/. - Icons: https://avaloniaui.github.io/icons.html — add to
Icons.axamlresource dictionary as needed. IMessengerfor cross-component communication.
- xUnit v3. Run tests with
dotnet run, notdotnet test.- Build the test project(s) before running tests:
dotnet build {project_path} --verbosity quiet - Whole test project:
dotnet run --project {project_path} - Specific class/method:
dotnet run --project {project_path} -- -filter /{assembly-name}/{namespace}/{class}/{method} - IMPORTANT: xUnit v3 filter paths use assembly name + C# namespace, which may look similar but are distinct. The assembly name is the project name.
- Example: for class
IdentityApiRegisterFilterTestsin namespaceControlR.Web.Server.Tests, projectControlR.Web.Server.Tests:-filter /ControlR.Web.Server.Tests/ControlR.Web.Server.Tests/IdentityApiRegisterFilterTests - For a specific method, append
/{method}.
- Example: for class
- Build the test project(s) before running tests:
- If the test filter doesn't find the test, rebuild the solution and try again.
- Use central package management via
Directory.Packages.props. - Don't add
Versionattributes to<PackageReference>elements in csproj files.Versionattributes go inDirectory.Packages.props. - Use
dotnet add <project> package <name>to add packages.
semantic_searchis available for searching the codebase. Use when information is needed about the codebase.brave-search_brave_web_searchis available for web searches. Use when external data is needed.
- Planning documents and implementation notes go in
/.plans/(not committed).