Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

Geofencing

GitHub GitHub stars for shinyorg/shiny
Downloads NuGet downloads for Shiny.Geofencing
Operating Systems
Android
iOS
Windows

Geofencing lets your app react when a user enters, exits, or stays in (dwells in) a geographic area — even when the app is not running. Unlike GPS tracking, geofencing is managed by the operating system and is highly battery efficient, making it ideal for location-triggered background tasks.

Feature Android iOS Windows
Max Regions 60 20 Unlimited
Background Support Yes Yes Yes
Google Play Fallback GPS-direct when unavailable N/A N/A
Dwell Native (loitering delay) Calculated by Shiny - usually reported at exit Timer in Shiny
Changed files
  • MyApp/
Delegates/MyGeofenceDelegate.cs
MyGeofenceDelegate.cs
1using Shiny.Locations;2​3namespace MyApp.Delegates;4​5public partial class MyGeofenceDelegate : IGeofenceDelegate6{7    public MyGeofenceDelegate()8    {9    }10​11    public Task OnStatusChanged(GeofenceState newStatus, GeofenceRegion region)12    {13        throw new NotImplementedException();14    }15}16​17#if ANDROID18public partial class MyGeofenceDelegate : IAndroidForegroundServiceDelegate19{20    public void Configure(AndroidX.Core.App.NotificationCompat.Builder builder)21    {22        23    }24}25#endif

Implement IGeofenceDelegate to handle geofence enter/exit events. This delegate fires in the background when a boundary is crossed.

public class MyGeofenceDelegate : IGeofenceDelegate
{
readonly ILogger<MyGeofenceDelegate> logger;
// Full dependency injection support - treat as a singleton
public MyGeofenceDelegate(ILogger<MyGeofenceDelegate> logger)
{
this.logger = logger;
}
public async Task OnStatusChanged(GeofenceState newStatus, GeofenceRegion region)
{
if (newStatus == GeofenceState.Entered)
{
this.logger.LogInformation("Entered region: {Region}", region.Identifier);
// send a notification, sync data, trigger an action, etc.
}
else if (newStatus == GeofenceState.Exited)
{
this.logger.LogInformation("Exited region: {Region}", region.Identifier);
}
else if (newStatus == GeofenceState.Dwelling)
{
// only for regions with a DwellTime - see "Dwell" below
this.logger.LogInformation("Dwelling in region: {Region}", region.Identifier);
}
}
}

Inject IGeofenceManager to manage geofence regions at runtime.

Always request access before starting monitoring:

IGeofenceManager geofenceManager; // injected
var access = await geofenceManager.RequestAccess();
if (access != AccessState.Available)
{
// Handle denied/restricted - show user messaging
return;
}
// Start monitoring a region
await geofenceManager.StartMonitoring(new GeofenceRegion(
"work",
new Position(43.6532, -79.3832), // latitude, longitude
Distance.FromMeters(200) // radius
));
// Stop monitoring a specific region
await geofenceManager.StopMonitoring("work");
// Stop monitoring all regions
await geofenceManager.StopAllMonitoring();

If you re-register the same regions on every launch, use TryStartMonitoring to avoid duplicates. It only starts monitoring when a region with that identifier isn’t already monitored, and — when replaceIfExists is true (the default) — stops and restarts an existing region so changed position or notification settings take effect. It returns true if the region already existed, false if it was newly added.

var region = new GeofenceRegion(
"work",
new Position(43.6532, -79.3832),
Distance.FromMeters(200)
);
// Idempotent: safe to call repeatedly without duplicating or throwing
bool alreadyExisted = await geofenceManager.TryStartMonitoring(region);
// Add without replacing an existing region of the same identifier
await geofenceManager.TryStartMonitoring(region, replaceIfExists: false);
var region = new GeofenceRegion(
Identifier: "coffee-shop",
Center: new Position(43.6532, -79.3832),
Radius: Distance.FromMeters(100),
SingleUse: false, // true = auto-remove after first trigger
NotifyOnEntry: true, // fire delegate on enter
NotifyOnExit: true // fire delegate on exit
)
{
DwellTime = TimeSpan.FromMinutes(5) // optional - fire delegate after staying inside this long
};
Parameter Type Default Description
Identifier string required Unique ID for the region
Center Position required Latitude/longitude of the circle center
Radius Distance required Radius of the circular geofence
SingleUse bool false Automatically stop monitoring after first trigger (after the dwell, when DwellTime is set)
NotifyOnEntry bool true Fire delegate when entering the region
NotifyOnExit bool true Fire delegate when exiting the region
DwellTime TimeSpan? null Fire delegate with GeofenceState.Dwelling once the device has stayed inside this long. Set with an object initializer

A dwell reports that the user has stayed in a region, not just passed through it. Set DwellTime and handle GeofenceState.Dwelling in your delegate. It is independent of the entry/exit flags, so a region can report only the dwell:

await geofenceManager.StartMonitoring(new GeofenceRegion(
"coffee-shop",
new Position(43.6532, -79.3832),
Distance.FromMeters(100),
NotifyOnEntry: false,
NotifyOnExit: false
)
{
DwellTime = TimeSpan.FromMinutes(5)
});

Entry and exit are always tracked, whatever the flags say. The flags only decide what reaches your delegate. Leaving before the dwell time is up means no Dwelling. On Android, iOS and GPS-direct, if the device is already inside when monitoring starts, no Entered is reported, but that time counts toward the dwell.

No GPS or extra permission is involved. When Dwelling arrives differs by platform:

Android Windows iOS
Dwell source Native loitering delay Shiny timer Shiny: calculated at exit, or a timer while the app runs
Dwelling arrives While still inside While still inside Usually at exit, just before Exited

For someone at the gym for 45 minutes with a 20-minute DwellTime:

Time Android Windows iOS
0:00 walk in Entered Entered Entered
0:20 Dwelling Dwelling nothing, unless the app is running
0:45 walk out Exited Exited Dwelling, then Exited
// Get all currently monitored regions
var regions = geofenceManager.GetMonitorRegions();
// Request the current state of a region (inside or outside)
var state = await geofenceManager.RequestState(region);
// GeofenceState.Entered, GeofenceState.Exited, or GeofenceState.Unknown
// Check current permission status without prompting
var status = geofenceManager.CurrentStatus;

Geofencing on Android runs on Google Play Services’ GeofencingClient. The GPS-direct fallback (AddGpsDirectGeofencing) was removed when geofencing moved into its own package, so devices without Google Play Services get no geofence events. If you need region checks on those devices, run Shiny.Gps and test your regions against each reading with region.IsPositionInside(reading.Position).

Shiny handles all the platform complexity behind a single API:

Platform Details
Android Uses Google Play Services geofencing when available, falls back to GPS-direct otherwise
iOS 18+ Uses the modern CLMonitor API
iOS < 18 Uses the legacy CLLocationManager region monitoring API
Windows Uses Windows.Devices.Geolocation.Geofencing

Monitored regions are automatically persisted and restored on app restart — you don’t need to re-register them.

claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One plugin installs all 48 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One plugin installs all 48 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

View shiny-locations Skill