SharpOnvifServer 0.10.0
dotnet add package SharpOnvifServer --version 0.10.0
NuGet\Install-Package SharpOnvifServer -Version 0.10.0
<PackageReference Include="SharpOnvifServer" Version="0.10.0" />
<PackageVersion Include="SharpOnvifServer" Version="0.10.0" />
<PackageReference Include="SharpOnvifServer" />
paket add SharpOnvifServer --version 0.10.0
#r "nuget: SharpOnvifServer, 0.10.0"
#:package SharpOnvifServer@0.10.0
#addin nuget:?package=SharpOnvifServer&version=0.10.0
#tool nuget:?package=SharpOnvifServer&version=0.10.0
SharpOnvif
A C# implementation of the Onvif interface - client as well as the server. All profiles are supported.
Upgrading from 0.9.x? 0.10.0 replaces WCF and CoreWCF with
HttpClientand ASP.NET Core, and merges the 50 per-service packages into two. Nothing changed on the wire. See doc/migration.md.
SharpOnvifServer
Onvif server provides NET8 and NET10 bindings generated from the Onvif WSDLs by
WsdlGenerator, hosted on ASP.NET Core. It makes it easy to implement only parts of the
Onvif specification needed for your project.
Start with a normal ASP.NET Core application:
var builder = WebApplication.CreateBuilder();
Add Digest authentication for Onvif:
builder.Services.AddSingleton<IUserRepository, UserRepository>();
builder.Services.AddOnvifDigestAuthentication();
Implement IUserRepository to provide user verification and configure your user:
public class UserRepository : IUserRepository
{
public string UserName { get; set; } = "admin";
public string Password { get; set; } = "password";
public Task<UserInfo> GetUser(string userName)
{
if (string.Compare(userName, UserName, false) == 0)
{
return Task.FromResult(new UserInfo() { UserName = userName, Password = Password });
}
return Task.FromResult((UserInfo)null);
}
public Task<UserInfo> GetUserAsync(string userName)
{
return Task.FromResult(GetUser(userName));
}
// used only when userhash=TRUE - see the examples for an implementation
public UserInfo GetUserByHash(string algorithm, string userName, string realm)
{
throw new NotImplementedException();
}
public Task<UserInfo> GetUserByHashAsync(string algorithm, string userName, string realm)
{
throw new NotImplementedException();
}
}
Optionally, add Onvif discovery to make your service discoverable on the network:
builder.Services.AddOnvifDiscovery();
The device answers a Probe, and announces itself with a WS-Discovery Hello when it starts and a Bye when it stops, so a client learns about it without having to probe. Every announcement names the same endpoint reference, which is how a client pairs the Bye with the device that said Hello. That address lasts as long as the process unless you give it one that outlives a restart:
builder.Services.AddOnvifDiscovery(new OnvifDiscoveryOptions
{
// From something the device keeps - its serial number or MAC - or a client sees a new
// device every time this one is restarted.
EndpointReference = "urn:uuid:" + deviceUuid,
});
Simple DeviceImpl just extends SharpOnvifServer.DeviceMgmt.DeviceBase and overrides a method you want to implement - for instance GetDeviceInformation:
public class DeviceImpl : DeviceBase
{
public override GetDeviceInformationResponse GetDeviceInformation(GetDeviceInformationRequest request)
{
return new GetDeviceInformationResponse()
{
FirmwareVersion = "1.0",
HardwareId = "1.0",
Manufacturer = "Manufacturer",
Model = "1",
SerialNumber = "1"
};
}
}
Each operation appears three times on the generated base, each layer defaulting to the next, so you can override whichever suits: an async form taking the request,
a synchronous form taking the request, and a synchronous form taking the request's members as arguments. Anything you do not override is reported to the client as
the ter:ActionNotSupported fault.
Add it as a singleton:
builder.Services.AddSingleton<DeviceImpl>();
Add authentication:
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Finally map the service onto a URL and run:
app.MapOnvifService<DeviceImpl>("/onvif/device_service");
app.Run();
Several services can share one URL, which is what real devices do - requests are routed by their SOAP action:
app.MapOnvifService<DeviceImpl>("/onvif/device_service");
app.MapOnvifService<MediaImpl>("/onvif/device_service");
app.MapOnvifService<PTZImpl>("/onvif/device_service");
The operation is found from the action parameter of the Content-Type header, a wsa:Action SOAP
header, or the body element, whichever the client sends - Onvif Device Manager uses the second form
for event subscriptions.
An address with a trailing segment reaches the same service, which is how Onvif addresses a
subscription manager: a request to /onvif/Events/PullPointSubscription/aV9xN2sMv1Qb0Zt8/ reaches the
service mapped at /onvif/Events/PullPointSubscription, with the segment available to the
implementation as HttpContext.Items[OnvifEvents.ONVIF_SUBSCRIPTION_ID] - a string, because a
subscription ID is an unguessable token rather than a counter.
Your Onvif service should now be discoverable on the network and you should be able to use Onvif Device Manager or similar tool to call your endpoint.
See Onvif.Server sample project for a complete example.
SharpOnvifClient
Onvif client provides .NET Standard 2.0, .NET Framework 4.8.1, NET8.0 and NET10.0 bindings generated
from the Onvif WSDLs by WsdlGenerator, over HttpClient. SimpleOnvifClient wraps common API calls
to get basic information from the camera and includes both Pull Point as well as Basic event subscriptions.
To discover Onvif devices on your network, use:
var discovery = new OnvifDiscoveryClient();
var onvifDevices = await discovery.DiscoverAsync();
A Probe only finds what is on the network at the moment it is sent, which is no help to an application that starts before its camera does. To wait for one to appear instead:
var device = await discovery.WaitForDeviceAsync(cancellationToken: stopping.Token);
It listens for the WS-Discovery Hello a device sends when it joins, and keeps probing as well - an announcement is UDP multicast, so it can be lost, and one sent before you started listening is already gone. Pass a predicate to wait for a particular device. There is no timeout, because "wait until the camera is switched on" has no natural one; cancel the token to stop.
Both listen on every interface that carries multicast, IPv4 and IPv6 alike.
OnvifDiscoveryListener is the same mechanism without the waiting, for an application that wants
to keep track of devices coming and going:
var listener = new OnvifDiscoveryListener();
listener.DeviceAnnounced += (s, e) => Console.WriteLine($"{e.Device.Name} arrived");
listener.DeviceLeft += (s, e) => Console.WriteLine($"{e.Device.Name} left");
listener.Start();
To create the SimpleOnvifClient, use:
var client = new SimpleOnvifClient(onvifDevices[0].Addresses[0], "admin", "password");
Call GetDeviceInformationAsync to retrieve information about the device:
var deviceInfo = await client.GetDeviceInformationAsync();
Call GetServicesAsync to retrieve a list of all services supported by the device:
var services = await client.GetServicesAsync();
Some operations require the device to support a service. For instance, to retrieve the stream URI the device must support the media service. To check whether the Onvif service is supported by the device, call:
if (services.Service.FirstOrDefault(x => x.Namespace == OnvifServices.MEDIA) != null)
{
// operation only available when the service is supported
}
Full list of services that can be supported by the device is available in SharpOnvifCommon.OnvifServices.
Delivering only what a subscriber asked for
A client subscribing to events says which it wants, and a device that sends it everything else as
well is not conformant. TopicFilter reads the filter out of the subscribe request and answers
whether a notification is one of them:
TopicFilter topics = TopicFilter.FromFilter(request.Filter);
...
if (topics.Matches(notification))
{
// queue it for this subscriber
}
It understands a concrete topic, a set of them separated by |, * for one level, and a trailing
//. for a topic and everything beneath it. An expression written in a dialect it cannot evaluate
matches everything, so a subscriber is never silently sent nothing.
A notification carries name/value pairs as tt:SimpleItem. Anything with a shape to it - a
rectangle, an analytics payload - goes in SourceElements or DataElements and is written as
tt:ElementItem:
var message = new NotificationMessage
{
Topic = "RuleEngine/CellMotionDetector/Motion",
Data = { { "IsMotion", "true" } },
DataElements = { { "Shape", rectangleElement } },
};
Pull Point event subscription
Pull point event subscription does not require any special networking configuration and it should work in most networks. To create a new Pull Point subscription, call:
var subscription = await client.PullPointSubscribeAsync();
To retrieve the current notifications from the Pull Point subscription, call:
var notifications = await client.PullPointPullMessagesAsync(subscription);
foreach (var notification in notifications)
{
// handle the notification message
bool? isMotion = SharpOnvifClient.OnvifEvents.IsMotionDetected(notification);
}
Basic event subscription
Basic event subscription utilizes a callback from the camera when an event occurs. This requires the camera to be able to reach your machine through a firewall/NAT.
To listen for incoming notifications, you must run SimpleOnvifEventListener:
// ID 1 will identify this camera in the callback
const int CAMERA1 = 1;
var eventListener = new SimpleOnvifEventListener();
eventListener.Start((int cameraID, string ev) =>
{
bool? isTamper = SharpOnvifClient.OnvifEvents.IsTamperDetected(ev);
if(cameraID == CAMERA1)
{
// handle the notification message for CAMERA1
}
});
var subscriptionResponse = await client.BasicSubscribeAsync(eventListener.GetOnvifEventListenerUri(CAMERA1));
Using the generated clients
Every Onvif service is in the SharpOnvifClient package, each under its own namespace
(SharpOnvifClient.DeviceMgmt, SharpOnvifClient.Media, SharpOnvifClient.PTZ, and so on), with
the shared Onvif data model in SharpOnvifCommon.Onvif. Create the client with the endpoint
address and, if the device requires them, credentials:
using (var deviceClient = new SharpOnvifClient.DeviceMgmt.DeviceClient(
"http://192.168.1.10/onvif/device_service", "admin", "password"))
{
var deviceInfo = await deviceClient.GetDeviceInformationAsync(new GetDeviceInformationRequest());
}
Both Onvif digest schemes are offered by default. For full control over authentication and transport, pass OnvifClientSettings:
var settings = new SharpOnvifCommon.Soap.OnvifClientSettings
{
Credentials = new System.Net.NetworkCredential("admin", "password"),
Authentication = new SharpOnvifCommon.Security.OnvifAuthenticationSettings(
SharpOnvifCommon.Security.DigestAuthentication.HttpDigest),
Timeout = TimeSpan.FromSeconds(30),
};
using (var deviceClient = new SharpOnvifClient.DeviceMgmt.DeviceClient(uri, settings))
{
var deviceInfo = await deviceClient.GetDeviceInformationAsync(new GetDeviceInformationRequest());
}
Every operation also has an overload that takes the request's members directly, so you rarely need to build the request yourself:
var services = await deviceClient.GetServicesAsync(includeCapability: false);
A device that cannot be reached, drops the connection, or does not answer in time raises
SharpOnvifCommon.Soap.SoapTransportException. A device is a thing that reboots and loses power,
and a pull point spends nearly all its time waiting on a request that any of those cuts short, so
a loop that polls one has to expect it:
while (true)
{
var subscription = await client.PullPointSubscribeAsync(60);
try
{
while (true)
{
var messages = await client.PullPointPullMessagesAsync(
subscription.SubscriptionReference.Address.Value);
// handle the notifications
}
}
catch (SoapTransportException)
{
// the device went away; it has forgotten the subscription, so make a new one
}
}
TimedOut tells a device that ran late from one that was not there at all, and InnerException
carries what the HTTP stack actually said. A cancellation you asked for is not this - that still
arrives as an OperationCanceledException.
A device that answers with a SOAP fault raises SharpOnvifCommon.Xml.SoapFaultException, which carries the Onvif error subcode:
try
{
await deviceClient.GetHostnameAsync();
}
catch (SoapFaultException fault) when (fault.Fault?.Subcode == "ActionNotSupported")
{
// the device does not implement this operation
}
See Onvif.Client sample project for a complete example.
Digest authentication
Onvif supports two types of Digest authentication. Legacy WS-UsernameToken authentication carried inside the SOAP headers and HTTP Digest authentication as defined in RFC 7616. Both types of authentication are now supported on both the client and the server.
Load balancing
HTTP Digest keeps two pieces of state on the server, and both are per process by default: the private key and the record of which nonces have been seen. To run this service behind a load balancer, you will have to store the private key and replay list in a shared location. Then you can configure the service as follows:
HttpDigestAuthentication.SetNoncePrivateKey(keyFromYourSecretStore);
builder.Services.AddOnvifDigestAuthentication(options =>
{
options.HttpDigestNonceReplayStore = new MyDistributedNonceReplayStore();
});
A device holds at most DefaultEventSubscriptionManager.MaxSubscriptions (1000) at once and
refuses the next with a fault. A subscription outlives the request that made it and is swept only
when it expires, so without a limit a client subscribing in a loop - broken as easily as hostile -
leaves a device holding every one it asked for.
INonceReplayStore has one method - it spends a nonce at a nonce count and says whether that count
had been seen before. MemoryNonceReplayStore, the default, holds the record in this process.
Logging
The client reports what it could not do through an ILog it is given. The logger belongs
to the object, not to the process, so an application watching several cameras can tell which one
is complaining - or send one of them nowhere:
var logger = new DefaultOnvifLogger { IsLoggingEnabled = true, IsDebugEnabled = false };
var client = new SimpleOnvifClient(uri, "admin", "password") { Logger = logger };
var listener = new SimpleOnvifEventListener(host) { Logger = logger };
var discovery = new OnvifDiscoveryClient(logger);
OnvifClientSettings.Logger does the same for a service client built directly. Given none, an
object reports nowhere. Implement ILog to send it into your own logging, or use
NullOnvifLogger.Instance to be explicit about silence. There is no dependency on any logging
package, which is what keeps these assemblies free of dependencies altogether. The server does not
use this - it is given an ILogger by the host and logs to that, and the two names are kept apart
deliberately so that one does not shadow the other where both are in scope.
No dependencies
SharpOnvifClient, SharpOnvifServer and SharpOnvifCommon reference no NuGet packages on any of
their target frameworks.
Testing
Only the DeviceMgmt, Media and Events were tested with Hikvision cameras. Server implementation was tested using Onvif Device Manager.
Credits
Special thanks to Piotr Stapp for figuring out the SOAP security headers in NET8: https://stapp.space/using-soap-security-in-dotnet-core/.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- SharpOnvifCommon (>= 0.10.0)
-
net8.0
- SharpOnvifCommon (>= 0.10.0)
NuGet packages (25)
Showing the top 5 NuGet packages that depend on SharpOnvifServer:
| Package | Downloads |
|---|---|
|
SharpOnvifServer.DeviceMgmt
A C# implementation of the Onvif interface - client as well as the server. All profiles are supported. |
|
|
SharpOnvifServer.Media
A C# implementation of the Onvif interface - client as well as the server. All profiles are supported. |
|
|
SharpOnvifServer.PTZ
A C# implementation of the Onvif interface - client as well as the server. All profiles are supported. |
|
|
SharpOnvifServer.Events
A C# implementation of the Onvif interface - client as well as the server. All profiles are supported. |
|
|
SharpOnvifServer.Imaging
A C# implementation of the Onvif interface - client as well as the server. All profiles are supported. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.10.0 | 0 | 9/14/2026 |
| 0.9.6 | 292 | 9/10/2026 |
| 0.9.5 | 1,033 | 6/12/2026 |
| 0.9.4 | 588 | 6/10/2026 |
| 0.9.3 | 679 | 5/20/2026 |
| 0.9.2 | 721 | 4/11/2026 |
| 0.9.1 | 577 | 3/1/2026 |
| 0.9.0 | 2,611 | 2/12/2026 |
| 0.8.1 | 353 | 2/7/2026 |
| 0.8.0 | 323 | 2/1/2026 |
| 0.7.1 | 425 | 12/30/2025 |
| 0.7.0 | 413 | 11/15/2025 |
| 0.6.1 | 728 | 11/13/2025 |
| 0.6.0 | 572 | 11/7/2025 |
| 0.5.2 | 669 | 10/27/2025 |
| 0.5.1 | 648 | 10/26/2025 |
| 0.5.0 | 1,150 | 10/25/2025 |
| 0.4.3 | 550 | 10/25/2025 |
| 0.4.2 | 691 | 8/27/2025 |
| 0.4.1 | 632 | 7/12/2025 |