Kebechet.Maui.Health
5.0.0
Prefix Reserved
dotnet add package Kebechet.Maui.Health --version 5.0.0
NuGet\Install-Package Kebechet.Maui.Health -Version 5.0.0
<PackageReference Include="Kebechet.Maui.Health" Version="5.0.0" />
<PackageVersion Include="Kebechet.Maui.Health" Version="5.0.0" />
<PackageReference Include="Kebechet.Maui.Health" />
paket add Kebechet.Maui.Health --version 5.0.0
#r "nuget: Kebechet.Maui.Health, 5.0.0"
#:package Kebechet.Maui.Health@5.0.0
#addin nuget:?package=Kebechet.Maui.Health&version=5.0.0
#tool nuget:?package=Kebechet.Maui.Health&version=5.0.0
Maui.Health
Abstraction around Android Health Connect and iOS HealthKit with unified API
Feel free to contribute ❤️
Features
- Cross-Platform: Works with Android Health Connect and iOS HealthKit
- Generic API: Use
GetHealthDataAsync<TDto>()for type-safe health data retrieval - Unified DTOs: Platform-agnostic data transfer objects with common properties
- Time Range Support: Duration-based metrics implement
IHealthTimeRangeinterface - Write/delete: Possibility to write/delete any health record or activity to/from Android Health/iOS HealthKit
- Aggregate: Platform-native aggregation (sum, average) with cross-source deduplication
- Aggregate by interval: Bucketed aggregation (daily, hourly) using native APIs
- Differential sync: Track changes (upserts/deletions) since a token for efficient data synchronization
- Duplication detection: If you write activity under your app to the ios/android health and at same time you start activity on watch/phone natively. You have possibility to detect these workouts and synchronize it as you need.
Platform Support & Health Data Mapping
| Health Data Type | Android Health Connect | iOS HealthKit | Wrapper Implementation |
|---|---|---|---|
| Steps | ✅ StepsRecord | ✅ StepCount | ✅ StepsDto |
| Weight | ✅ WeightRecord | ✅ BodyMass | ✅ WeightDto |
| Height | ✅ HeightRecord | ✅ Height | ✅ HeightDto |
| Heart Rate | ✅ HeartRateRecord | ✅ HeartRate | ✅ HeartRateDto |
| Active Calories | ✅ ActiveCaloriesBurnedRecord | ✅ ActiveEnergyBurned | ✅ ActiveCaloriesBurnedDto |
| Exercise Session | ✅ ExerciseSessionRecord | ✅ Workout | ✅ WorkoutDto |
| Blood Glucose | ✅ BloodGlucoseRecord | ✅ BloodGlucose | ❌ N/A |
| Body Temperature | ✅ BodyTemperatureRecord | ✅ BodyTemperature | ❌ N/A |
| Oxygen Saturation | ✅ OxygenSaturationRecord | ✅ OxygenSaturation | ❌ N/A |
| Respiratory Rate | ✅ RespiratoryRateRecord | ✅ RespiratoryRate | ❌ N/A |
| Basal Metabolic Rate | ✅ BasalMetabolicRateRecord | ✅ BasalEnergyBurned | ❌ N/A |
| Body Fat | ✅ BodyFatRecord | ✅ BodyFatPercentage | ✅ BodyFatDto |
| Lean Body Mass | ✅ LeanBodyMassRecord | ✅ LeanBodyMass | ❌ N/A |
| Hydration | ✅ HydrationRecord | ✅ DietaryWater | ❌ N/A |
| VO2 Max | ✅ Vo2MaxRecord | ✅ VO2Max | ✅ Vo2MaxDto |
| Resting Heart Rate | ✅ RestingHeartRateRecord | ✅ RestingHeartRate | ❌ N/A |
| Heart Rate Variability | ✅ HeartRateVariabilityRmssdRecord | ✅ HeartRateVariabilitySdnn | ❌ N/A |
| Blood Pressure | ✅ BloodPressureRecord | ✅ Split into Systolic/Diastolic | 🚧 WIP (commented out) |
Usage
1. Registration
Register the health service in your MauiProgram.cs:
builder.Services.AddHealth();
Then setup all Android and iOS necessities.
- Android (4) docs, docs2
- in Google Play console give Health permissions to the app
- for successful app approval your Policy page must contain
Health data collection and use,Data retention policy - change of
AndroidManifest.xml+ new activity showing privacy policy - add
<queries>element toAndroidManifest.xml(inside<manifest>, outside<application>):
This is required on Android 11–13 due to package visibility filtering. Without it,<queries> <package android:name="com.google.android.apps.healthdata" /> </queries>getSdkStatus()cannot detect Health Connect even when it's installed, causing permission requests to silently fail. On Android 14+ Health Connect is a system service so this isn't strictly needed, but it does no harm. - change of min. Android version to v26
- reading while the app is not in the foreground needs one more permission, which this library
deliberately does not declare — permissions are the consuming app's to own, and Google Play holds
the app accountable for them:
Declaring it is not enough on its own: Health Connect permissions are granted at runtime, so also pass<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND"/>canRequestBackgroundReadPermission: truetoRequestPermissions. The request is skipped automatically on devices whose Health Connect version does not support the feature. Without both halves, a read or aggregate that lands after the app leaves the foreground fails withSecurityException: … must be in foreground. See background reads.
- iOS (3) docs, docs2
- generating new provisioning profile containing HealthKit permissions. These permissions are changed in Identifiers
- adding
Entitlements.plist - adjustment of
Info.plist- ⚠️ Beware, if your app already exists and targets various devices adding
UIRequiredDeviceCapabilitieswithhealthkitcan get your release rejected. For that reason I ommited adding this requirement and I just make sure that I check if the device is capable of usinghealthkit.
- ⚠️ Beware, if your app already exists and targets various devices adding
After you have everything setup correctly you can use IHealthService from DI container and call it's methods.
If you want an example there is a DemoApp project showing number of steps for Current day
2. Basic Usage
public class HealthExampleService
{
private readonly IHealthService _healthService;
public HealthExampleService(IHealthService healthService)
{
_healthService = healthService;
}
public async Task<List<StepsDto>> GetTodaysStepsAsync()
{
if (!_healthService.IsSupported)
{
return [];
}
var timeRange = HealthTimeRange.FromDateTime(DateTime.Today, DateTime.Now);
return await _healthService.GetHealthData<StepsDto>(timeRange);
}
}
3. Working with Time Ranges
Duration-based metrics implement IHealthTimeRange:
public async Task AnalyzeStepsData()
{
var timeRange = HealthTimeRange.FromDateTime(DateTime.Today, DateTime.Now);
var steps = await _healthService.GetHealthData<StepsDto>(timeRange);
foreach (var stepRecord in steps)
{
// Common properties from BaseHealthMetricDto
Console.WriteLine($"ID: {stepRecord.Id}");
// DataOrigin is the stable bundle identifier (iOS) or package name (Android), or null
// when the platform exposes no source metadata. Safe to compare against your own
// bundle/package identifier to check ownership.
Console.WriteLine($"Source: {stepRecord.DataOrigin ?? "<unknown>"}");
Console.WriteLine($"Recorded: {stepRecord.Timestamp}");
// Steps-specific data
Console.WriteLine($"Steps: {stepRecord.Count}");
// Time range data (IHealthTimeRange)
Console.WriteLine($"Period: {stepRecord.StartTime} to {stepRecord.EndTime}");
Console.WriteLine($"Duration: {stepRecord.Duration}");
// Type-safe duration checking
if (stepRecord is IHealthTimeRange timeRange)
{
Console.WriteLine($"This measurement lasted {timeRange.Duration.TotalMinutes} minutes");
}
}
}
4. Read Single Record
Fetch a specific health record by its platform-specific ID (Android: Health Connect metadata ID, iOS: HealthKit UUID):
public async Task<StepsDto?> GetSpecificRecord(string recordId)
{
return await _healthService.GetHealthRecord<StepsDto>(recordId);
}
Note: This API is marked
[Experimental("MH001")]. Suppress the warning with#pragma warning disable MH001.
5. Delete Health Records
Delete any health record by its platform-specific ID. You can only delete records created by your application:
public async Task DeleteRecord(string recordId)
{
var isDeleted = await _healthService.DeleteHealthData<StepsDto>(recordId);
if (isDeleted)
{
Console.WriteLine("Record deleted successfully");
}
}
Note: This API is marked
[Experimental("MH002")]. Suppress the warning with#pragma warning disable MH002.
6. Aggregated Health Data
Get deduplicated totals or averages using platform-native aggregation. This uses Android's aggregate() API and iOS's HKStatisticsQuery, which properly handle data from multiple health apps (e.g., Samsung Health + Google Fit):
public async Task ShowTodaysSummary()
{
var todayRange = HealthTimeRange.FromDateTime(DateTime.Today, DateTime.Now);
// Cumulative types (steps, calories) return a sum
var steps = await _healthService.GetAggregatedHealthData<StepsDto>(todayRange);
if (steps is not null)
{
Console.WriteLine($"Total steps today: {steps.Value}");
}
// Discrete types (weight, heart rate) return an average
var weight = await _healthService.GetAggregatedHealthData<WeightDto>(todayRange);
if (weight is not null)
{
Console.WriteLine($"Average weight: {weight.Value} {weight.Unit}");
}
}
Note: This API is marked
[Experimental("MH003")]. Suppress the warning with#pragma warning disable MH003.
7. Aggregated Health Data by Interval
Get aggregated data bucketed by time intervals - ideal for charts and day-by-day views. Uses Android's aggregateGroupByDuration() and iOS's HKStatisticsCollectionQuery:
public async Task ShowWeeklySteps()
{
var weekRange = HealthTimeRange.FromDateTime(
DateTime.Today.AddDays(-6), DateTime.Now);
var dailySteps = await _healthService.GetAggregatedHealthDataByInterval<StepsDto>(
weekRange, TimeSpan.FromDays(1));
foreach (var bucket in dailySteps)
{
Console.WriteLine($"{bucket.StartTime:ddd MMM dd}: {bucket.Value:N0} steps");
}
}
Note: This API is marked
[Experimental("MH004")]. Suppress the warning with#pragma warning disable MH004.
8. Differential Sync (Change Tracking)
Track changes (upserts and deletions) to health data since a given point in time. Uses Android's getChangesToken()/getChanges() and iOS's HKAnchoredObjectQuery. Tokens expire after 30 days.
public class HealthSyncService
{
private readonly IHealthService _healthService;
private string? _syncToken;
// Call once to establish a baseline - captures the current state
public async Task InitializeSync()
{
var dataTypes = new List<HealthDataType>
{
HealthDataType.Steps,
HealthDataType.Weight,
HealthDataType.ActiveCaloriesBurned
};
_syncToken = await _healthService.GetChangesToken(dataTypes);
// Store _syncToken persistently (e.g., Preferences, database)
}
// Call periodically to get new changes since last sync
public async Task SyncChanges()
{
if (_syncToken is null) return;
var result = await _healthService.GetChanges(_syncToken);
if (result is null) return;
foreach (var change in result.Changes)
{
Console.WriteLine($"{change.Type}: {change.RecordId}");
}
// Update token for next call
_syncToken = result.NextToken;
// If more changes available, keep fetching
if (result.HasMore)
{
await SyncChanges();
}
}
}
Note: These APIs are marked
[Experimental("MH005")]and[Experimental("MH006")]. Suppress with#pragma warning disable MH005, MH006.
9. Permission Handling
public async Task RequestPermissions()
{
var permissions = new List<HealthPermissionDto>
{
new() { HealthDataType = HealthDataType.Steps, PermissionType = PermissionType.Read },
new() { HealthDataType = HealthDataType.Weight, PermissionType = PermissionType.Read },
new() { HealthDataType = HealthDataType.Height, PermissionType = PermissionType.Read }
};
var result = await _healthService.RequestPermissions(permissions);
if (result.IsSuccess)
{
Console.WriteLine("Permissions granted!");
}
else
{
Console.WriteLine($"Permission error: {result.Error}");
}
}
Handling Health Connect Updates (Android)
On Android devices with API < 34, Health Connect is a separate app that may need to be installed or updated. The library returns a specific error so you can show custom UI before opening the Play Store:
public async Task RequestPermissionsWithUpdateHandling()
{
var permissions = new List<HealthPermissionDto>
{
new() { HealthDataType = HealthDataType.Steps, PermissionType = PermissionType.Read }
};
var result = await _healthService.RequestPermissions(permissions);
if (result.Error == RequestPermissionError.SdkUnavailableProviderUpdateRequired)
{
// Show your custom UI explaining the update requirement
bool userConfirmed = await DisplayAlert(
"Update Required",
"Health Connect needs to be updated to use health features.",
"Update", "Cancel");
if (userConfirmed)
{
_healthService.OpenStorePageOfHealthProvider(); // Opens Play Store
}
}
}
10. Workout Management (IHealthWorkoutService)
The Activity property on IHealthService provides workout/exercise session management (IHealthWorkoutService) with support for real-time tracking, pause/resume functionality, and duplicate detection.
Reading Workouts
public async Task<List<WorkoutDto>> GetTodaysWorkouts()
{
var timeRange = HealthTimeRange.FromDateTime(DateTime.Today, DateTime.Now);
var workouts = await _healthService.Activity.Read(timeRange);
foreach (var workout in workouts)
{
Console.WriteLine($"{workout.ActivityType}: {workout.StartTime:HH:mm} - {workout.EndTime:HH:mm}");
Console.WriteLine($"Duration: {workout.DurationSeconds / 60} minutes");
Console.WriteLine($"Source: {workout.DataOrigin ?? "<unknown>"}");
if (workout.EnergyBurned.HasValue)
Console.WriteLine($"Calories: {workout.EnergyBurned:F0} kcal");
if (workout.AverageHeartRate.HasValue)
Console.WriteLine($"Avg HR: {workout.AverageHeartRate:F0} BPM");
}
return workouts;
}
Writing Workouts
public async Task WriteCompletedWorkout()
{
var workout = new WorkoutDto
{
Id = Guid.NewGuid().ToString(),
DataOrigin = "com.companyname.MyApp", // Stable bundle identifier (iOS) / package name (Android). Ignored on write — platform stamps its own source.
ActivityType = ActivityType.Running,
Title = "Morning Run",
StartTime = DateTimeOffset.Now.AddMinutes(-30),
EndTime = DateTimeOffset.Now,
EnergyBurned = 250,
Distance = 5000 // meters
};
await _healthService.Activity.Write(workout);
}
Live Workout Session (Start/Pause/Resume/End)
Track workouts in real-time with pause/resume support:
public class WorkoutTracker
{
private readonly IHealthService _healthService;
// Start a new workout session
public async Task StartWorkout()
{
await _healthService.Activity.Start(
ActivityType.Running,
title: "Morning Run"
);
}
// Pause the active session
public async Task PauseWorkout()
{
await _healthService.Activity.Pause();
}
// Resume from pause
public async Task ResumeWorkout()
{
await _healthService.Activity.Resume();
}
// End session and save to health store
public async Task<WorkoutDto?> EndWorkout()
{
// Returns the completed workout saved to Health Connect/HealthKit
return await _healthService.Activity.End();
}
// Check session status
public async Task<bool> IsWorkoutRunning() => await _healthService.Activity.IsRunning();
public async Task<bool> IsWorkoutPaused() => await _healthService.Activity.IsPaused();
}
Duplicate Detection
When users track workouts from both your app and a smartwatch, duplicates can occur. The FindDuplicates method identifies these by matching:
- Same activity type
- Different data sources (e.g., "MyApp" vs "Apple Watch")
- Start/end times within a configurable threshold
public async Task DetectDuplicateWorkouts()
{
var timeRange = HealthTimeRange.FromDateTime(DateTime.Today, DateTime.Now);
var workouts = await _healthService.Activity.Read(timeRange);
// Find duplicates with 5-minute threshold
var duplicates = _healthService.Activity.FindDuplicates(
workouts,
dataOrigin: "com.companyname.MyApp", // Your app's bundle identifier (iOS) / package name (Android)
timeThresholdMinutes: 5 // Max time difference to consider as duplicate
);
foreach (var group in duplicates)
{
// Get the workout from your app
var appWorkout = group.AppWorkout;
// Get the workout from watch/other source
var externalWorkout = group.ExternalWorkout;
Console.WriteLine($"Duplicate found:");
Console.WriteLine($" App: {appWorkout?.DataOrigin} at {appWorkout?.StartTime:HH:mm}");
Console.WriteLine($" External: {externalWorkout?.DataOrigin} at {externalWorkout?.StartTime:HH:mm}");
Console.WriteLine($" Time diff: {group.StartTimeDifferenceMinutes:F1} minutes");
// User can decide which to keep - typically keep the watch data
// as it has more accurate heart rate and calorie data
if (appWorkout != null)
{
await _healthService.Activity.Delete(appWorkout);
}
}
}
Testing Tips
iOS Simulator/Device:
- If no health data exists, open the Health app
- Navigate to the desired metric (e.g., Steps)
- Tap "Add Data" in the top-right corner
- Manually add test data for development
Android Emulator:
- Install Google Health Connect app
- Add sample health data for testing
- Ensure proper permissions are granted
Credits
- @aritchie -
https://github.com/shinyorg/Health - @0xc3u -
https://github.com/0xc3u/Plugin.Maui.Health - @EagleDelux -
https://github.com/EagleDelux/androidx.health-connect-demo-.net-maui - @b099l3 -
https://github.com/b099l3/ios-samples/tree/65a4ab1606cfd8beb518731075e4af526c4da4ad/ios8/Fit/Fit
Other Sources
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0-android36.0 is compatible. net10.0-ios26.0 is compatible. net10.0-maccatalyst26.0 is compatible. net10.0-windows10.0.19041 is compatible. |
-
net10.0-android36.0
- Microsoft.Maui.Controls (>= 10.0.1)
- UnitsNet (>= 5.75.0)
- Xamarin.AndroidX.Activity.Ktx (>= 1.12.4.1)
- Xamarin.AndroidX.Fragment.Ktx (>= 1.8.9)
- Xamarin.AndroidX.Health.Connect.ConnectClient (>= 1.1.0.2)
- Xamarin.AndroidX.Lifecycle.LiveData (>= 2.10.0.2)
- Xamarin.AndroidX.Lifecycle.LiveData.Core (>= 2.10.0.2)
- Xamarin.AndroidX.Lifecycle.LiveData.Core.Ktx (>= 2.10.0.2)
- Xamarin.AndroidX.Lifecycle.Process (>= 2.10.0.2)
- Xamarin.AndroidX.Lifecycle.Runtime.Ktx (>= 2.10.0.2)
- Xamarin.AndroidX.Lifecycle.Runtime.Ktx.Android (>= 2.10.0.2)
- Xamarin.AndroidX.Lifecycle.ViewModel.Ktx (>= 2.10.0.2)
- Xamarin.AndroidX.SavedState (>= 1.4.0.2)
- Xamarin.AndroidX.SavedState.SavedState.Ktx (>= 1.4.0.2)
-
net10.0-ios26.0
- Microsoft.Maui.Controls (>= 10.0.1)
- UnitsNet (>= 5.75.0)
-
net10.0-maccatalyst26.0
- Microsoft.Maui.Controls (>= 10.0.1)
- UnitsNet (>= 5.75.0)
-
net10.0-windows10.0.19041
- Microsoft.Maui.Controls (>= 10.0.1)
- UnitsNet (>= 5.75.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 5.0.0 | 100 | 9/11/2026 |
| 4.0.0 | 296 | 6/10/2026 |
| 4.0.0-preview.10 | 182 | 5/13/2026 |
| 4.0.0-preview.9 | 222 | 5/9/2026 |
| 4.0.0-preview.8 | 88 | 5/4/2026 |
| 4.0.0-preview.7 | 78 | 4/26/2026 |
| 4.0.0-preview.6 | 79 | 4/21/2026 |
| 4.0.0-preview.5 | 79 | 4/20/2026 |
| 4.0.0-preview.4 | 68 | 4/19/2026 |
| 4.0.0-preview.3 | 79 | 4/17/2026 |
| 4.0.0-preview.2 | 106 | 4/8/2026 |
| 4.0.0-preview.1 | 92 | 4/3/2026 |
| 3.1.0-preview.2 | 83 | 4/3/2026 |
| 3.1.0-preview.1 | 83 | 3/27/2026 |
| 3.0.0 | 196 | 3/24/2026 |
| 2.0.2 | 287 | 1/23/2026 |
| 2.0.0-preview9 | 171 | 12/30/2025 |
| 2.0.0-preview14 | 170 | 12/31/2025 |
| 2.0.0-preview13 | 127 | 12/31/2025 |
| 2.0.0-preview12 | 130 | 12/31/2025 |
v5.0.0 — the Health Connect background-read permission can now be requested.
Health Connect rejects reads and aggregates made while the app is not in the foreground unless READ_HEALTH_DATA_IN_BACKGROUND has been granted, and a sync does not have to intend to run in the background to land there: a delayed or resumed one gets there on its own if the user leaves the app first. The permission is tied to no data type, so it could not be expressed through HealthPermissionDto and was previously impossible to ask for.
BREAKING CHANGES
• RequestPermission and RequestPermissions take a new canRequestBackgroundReadPermission flag BEFORE the CancellationToken. Callers passing the token positionally must name it (cancellationToken: token) or pass the new argument. The compiler catches every case — a CancellationToken will not bind to a bool. Both flags default to false, so behaviour is unchanged unless opted into.
NEW
• canRequestBackgroundReadPermission adds READ_HEALTH_DATA_IN_BACKGROUND to the Health Connect permission request, so reads and aggregates that land after the app leaves the foreground are no longer rejected. Android only; iOS ignores it, since HealthKit arranges background delivery per query type rather than by a permission.
• The request is gated on the device feature as well as the flag. On a Health Connect version that does not support background reads the permission is skipped, rather than being added to a request the user could never satisfy — which would leave the system dialog re-opening on every call.
• The manifest declaration stays with the consuming app: this package ships no AndroidManifest.xml and declares no health permission. Add <uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND"/> yourself, and note that declaring alone is not enough because Health Connect permissions are granted at runtime. See the README's Android setup section.
PREVIOUS RELEASE — v4.0.0
BREAKING CHANGES
• GetHealthData now returns HealthDataReadResult<TDto> instead of List<TDto>, so a failed platform call (Result.IsError) is no longer indistinguishable from "no records". Read the records off Result on success.
• WriteHealthData now returns WriteHealthDataResult instead of bool (#44), carrying typed errors and the platform-assigned record IDs.
• HealthTimeRange: removed the redundant date/time properties.
NEW
• Bulk write: WriteHealthData(IList<TDto>) writes in a single platform call and returns record IDs 1:1 with the input, so app-authored records link to their native counterpart without a reconciling read.
• UpdateHealthData (#45) — in-place update on Android (Health Connect updateRecords) and atomic sync-identifier replacement on iOS (HealthKit), with typed iOS-only failure modes. [Experimental MH007]
• DeleteHealthData — delete an app-authored record by ID. [Experimental MH002]
• GetHealthRecord — fetch a single record by platform ID. [Experimental MH001]
• Native aggregation: GetAggregatedHealthData, GetAggregatedHealthDataByInterval (TimeSpan buckets, time-zone aligned), and GetAggregatedHealthDataByCalendarPeriod (calendar-aware day/week/month/year buckets respecting DST, variable month length, and leap years). Uses platform-native aggregation that deduplicates across health apps. [Experimental MH003/MH004]
• Change tracking: GetChangesToken / GetChanges for delta sync of upserts and deletions. [Experimental MH005/MH006]
• GetPermissionStatuses — per-permission authorization status without triggering the permission UI.
• GetEarliestAccessibleDateTime — earliest UTC date reads are currently allowed from, abstracting per-platform history rules.
• Write DTOs via IHealthWritable, plus unit-aware conversions (unit enums and conversion constants) for both read and write.
• Live workout tracking via the Activity service: pause/resume and automatic duplicate detection.
FIXES & IMPROVEMENTS
• Unified DataOrigin value across platforms (#48).
• iOS measurement update (#47).
• Android: faster and more correct reflection resolving, fixed a potential lookup/search miss, and reduced minor perf overhead.
• Wide-window daily aggregation on Android no longer throws "Number of groups must not exceed 5000" when the requested range crosses a net DST shift (e.g. 1970-01-01 to today in Europe/Prague). The chunker now reserves one bucket of headroom because the period-dispatch path counts calendar days, not fixed 24h slots (#53).
• Fixed disposal of hanging references and a crash when calling health methods through Task.Run.
• Error swallowing fixed — runtime failures now surface through the Result types instead of being silently dropped.