A set of NuGet-published .NET libraries providing foundational infrastructure for building desktop and mobile applications. Current version: 2.3.0.0.
| Project | NuGet Package | Purpose |
|---|---|---|
Core |
CarinaStudio.AppBase.Core |
Foundation: extensions, threading, observables, collections |
Configuration |
CarinaStudio.AppBase.Configuration |
Settings management with JSON/XML serializers |
Application |
CarinaStudio.AppBase.Application |
Desktop application infrastructure and abstractions |
Avalonia |
CarinaStudio.AppBase.Avalonia |
Avalonia UI controls and extensions |
Application.Avalonia |
CarinaStudio.AppBase.Application.Avalonia |
Application implementation on top of Avalonia |
Application.Android |
CarinaStudio.AppBase.Application.Android |
Application implementation for Android |
MacOS |
CarinaStudio.AppBase.MacOS |
Native macOS API bindings via P/Invoke |
AutoUpdate |
CarinaStudio.AppBase.AutoUpdate |
Self-update for portable applications |
Tests |
CarinaStudio.AppBase.Tests |
Shared test helper utilities (also a NuGet package) |
Documentation |
— | DocFX-generated API docs site |
Core
├── Configuration (→ Core)
│ └── Application (→ Configuration + Microsoft.Extensions.Logging.Abstractions)
│ ├── Application.Avalonia (→ Application + Avalonia)
│ ├── Application.Android (→ Application + Configuration + Core)
│ └── AutoUpdate (→ Application)
├── MacOS (→ Core)
│ └── Avalonia (→ MacOS + Microsoft.Extensions.Logging.Abstractions)
└── Tests (→ Core)
- Shared properties:
Directory.Build.props— version, authors, Avalonia version, AOT settings - Target frameworks:
net6.0throughnet10.0(Android projects:net8.0-androidthroughnet10.0-android) - Language: C# with
LangVersion=preview - AOT: All projects are
IsAotCompatible=true - Packaging scripts:
BuildPackages.bat/BuildPackages.sh
When a change affects the architecture of a library project, update the corresponding AGENTS.md in that project's folder so the documentation stays in sync with the code.
- Nullable reference types are enabled (
#nullable enable) everywhere. - Compare native handles against
IntPtr.Zeroexplicitly (handle == IntPtr.Zero), notdefault. - Never pass
defaultas an argument — always use an explicit value (e.g.CancellationToken.None,TimeSpan.Zero). .Setup()forIDisposableinitialization — when creating anIDisposableand setting its properties immediately, do not use object-initializer syntax (new Foo { Prop = value }): if the initializer throws, the instance is never disposed. Use the.Setup(it => ...)extension instead, which guaranteesDispose()is called when the setup action throws.- Time units — milliseconds are the default. Bare
Timeout/Delay/Intervalnames are always milliseconds; do not appendMs. Use a unit suffix only when the value is not in milliseconds (SomethingSeconds,SomethingMicroseconds,SomethingTicks). - When a property needs custom accessor logic (validation, change notification, etc.), prefer the C#
fieldkeyword over a manually-declared backing field. - To pin a managed array or buffer, prefer the
fixedstatement for scope-bound pinning; reach forGCHandle.Alloc(…, GCHandleType.Pinned)only when the pin must outlive the current scope. - To read fields of a native interop struct from a raw byte buffer, mark the method
unsafeand access the struct in place through afixedpointer (fixed (byte* p = buffer) { var header = (SomeHeader*)p; … header->field … }) rather than copying the whole struct out (*(SomeHeader*)p) orMemoryMarshal.Read<T>(); usesizeof(SomeHeader)for the length check and offsets rather than a hardcoded byte count. If the method also needs to be awaitable, keep it non-asyncand return theTaskdirectly (anawaitcannot sit in theunsafecontext, and a pointer cannot be held across it). - Do not combine assignment and evaluation into the same expression — assign in its own statement, then use the value (e.g. lazy caching is
Field ??= Create(); return Field;, neverreturn Field ??= Create();or an expression-bodied member doing both). - Unsafe blocks are allowed globally (set in
Directory.Build.props). - All public async methods return
TaskorValueTask; UI-thread operations use the application's dispatcher. - Root namespace:
CarinaStudio(orCarinaStudio.<Module>for platform-specific projects). InternalsVisibleTois set in the library's.csproj, not inAssemblyInfo.IsTrimmable=Trueassembly metadata is set on all AOT-compatible projects.ObservableProperty<T>fields are namedXxxProp(e.g.MessageProp), neverXxxProperty, regardless of visibility. Reason: Avalonia 12 compiled bindings resolve any public static field named<Member>Propertyon the bound type as anAvaloniaPropertyand fail compilation when it is not one; thePropsuffix is applied to all visibilities for consistency.AvaloniaPropertyfields on controls keep the standardXxxPropertysuffix — that convention is required by Avalonia and does not conflict. The existingXxxProperty-suffixedObservablePropertyfields (e.g. inAutoUpdate/ViewModels/UpdatingSession.cs) are renamed as part of the Avalonia 12 upgrade; usePropin all new code.
- One type per file; file name matches the type name exactly.
- Each subsystem gets its own subfolder (e.g.
Threading/,Collections/,ViewModels/). - Namespace matches the folder path.
- Companion types for an interface (
Extensions, enums) go in separate files in the same folder. - Inner types within a class/file are ordered alphabetically by name.
- Members within a type (enum values, properties, methods) are also ordered alphabetically. Exception: struct fields with
[StructLayout(LayoutKind.Sequential)]must preserve their memory-layout order. extensionblocks (C# 14 extension members) are placed first in the containing class, before all other members; they are not sorted with other members. Members inside anextensionblock are ordered alphabetically.- Blank lines between members — two blank lines between members of a top-level type; one blank line between members of an inner (nested) type.
- Every public member carries an XML doc comment (
/// <summary>); use/// <inheritdoc/>in implementations. - Extension method classes are named
XxxExtensionsand placed in their own file. - When extending a type, prefer an extension property inside an
extension(T value)block over aGetX()-style extension method, whenever the accessor is a pure, side-effect-free projection that reads naturally as a property — so call sites readvalue.BaseNamerather thanvalue.GetBaseName(). - XML documentation is generated for both Debug and Release configurations.
- Suppress
CA1416only when calling APIs annotated with[SupportedOSPlatform]by the .NET runtime. - Custom P/Invoke definitions do not carry that annotation and do not require CA1416 suppression at their call sites.
MacOSproject achieves AOT compatibility by dispatching Objective-C runtime interop through libffi instead of dynamic code generation.
Rules that apply only within one library project — everything above applies solution-wide. Each project's own AGENTS.md documents its architecture, not its rules.
Application— logging goes through anILoggerobtained fromIApplication; never construct loggers directly.Configuration— settings keys are strongly typed: useSettingKey<T>instances as keys rather than raw strings.MacOS—- All P/Invoke calls go through named library handles in
NativeLibraryHandles— never hardcode dylib paths inline. - Sending messages with arbitrary signatures and dispatching defined methods go through libffi (
Ffi/LibFfi.cs), notReflection.Emit; call interfaces, closures and struct type descriptors are cached in native memory for the process lifetime. - Instance variables are read/written directly at
instance + Variable.Offset— never throughobject_getInstanceVariable/object_setInstanceVariable, which treat the ivar as a single pointer-sized value (the value itself, not a buffer address).Variable.Offsetre-resolves theIvarhandle by name on first use, because handles obtained duringClass.DefineClass(beforeobjc_registerClassPair) become dangling once laterclass_addIvarcalls reallocate the ivar list. - Structure types added to the library which may pass through native calls must also be listed in
ILLink.Descriptors.xml.
- All P/Invoke calls go through named library handles in
Each library project has a companion *.Tests project. Run tests with:
dotnet testTest projects are not published to NuGet and do not have their own AGENTS.md.
- Logic is correct for all paths, including edge cases (empty collections, null values, zero counts).
- Multi-step operations that must be atomic are protected by a lock or semaphore across all steps, not just individual operations.
- State mutations under a lock do not leak mutable references that can be read or written outside the lock.
async/awaitis used correctly — no fire-and-forget unless intentional; no.Resultor.Wait()blocking on async code.CancellationTokenis propagated through all async calls;OperationCanceledExceptionis not swallowed.IDisposableresources are disposed in all paths, including error paths.- Native handles are released on every path, and compared against
IntPtr.Zerorather thandefault.
- Shared mutable fields accessed from multiple threads are protected consistently.
- No TOCTOU (time-of-check/time-of-use) races — check and act happen under the same lock or synchronization primitive.
- Background-thread methods are marked
[CalledOnBackgroundThread]; UI-thread calls are dispatched viaSynchronizationContextor guarded withCheckAccess().
- Exceptions are not silently swallowed — at minimum log the error.
- Expected failure paths (file missing, unsupported platform) are logged at
Warning; unexpected exceptions atError. - Best-effort operations (e.g. cleanup) catch and log per-item rather than aborting the entire operation.
- All coding style rules above are followed (naming, formatting, nullability, patterns).
- Unused
usingdirectives removed; correct namespaces imported for any new types introduced. defaultis not passed as an argument — explicit values used instead.- Members within a type are still ordered alphabetically, and
extensionblocks still sit first in the containing class. Verify after adding, renaming, or moving any member. - Every public member carries an XML doc comment; implementations and overrides use
/// <inheritdoc/>.
- Check whether the change affects the architecture of a library project. If so, the corresponding project's
AGENTS.mdshould be updated to match.