Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions Docs/Sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ an `ISession` facade that wraps a raw `Session` and adds:
failover, so consumers see one transparent retry rather than a torn
state.
- A **default V2 subscription engine** so the new options-based
subscription API (`ISubscriptionManager`) is available on
`ManagedSession.SubscriptionManager` out of the box.
subscription API (`ISubscriptionManager`) is available via
`ISession.TryGetSubscriptionManager(out var manager)` out of the box.

```csharp
ManagedSession session = await ManagedSession.CreateAsync(
Expand Down Expand Up @@ -799,8 +799,9 @@ the `ISubscriptionEngine` contract. It owns:
`OnKeepAliveNotificationAsync`.

`DefaultSubscriptionEngine.SubscriptionManager` exposes the
`ISubscriptionManager` API; on `ManagedSession` it surfaces directly as
`SubscriptionManager`.
`ISubscriptionManager` API; on any `ISession` it is surfaced through
`TryGetSubscriptionManager(out var manager)`, which returns `true` and the
manager for V2-engine sessions and `false` for classic-engine sessions.

```csharp
// V2 engine is the default for ManagedSession
Expand Down Expand Up @@ -959,11 +960,12 @@ channel/transport layer.
|---|---|
| Existing code using `session.AddSubscription(new Subscription(...))` and the classic event-driven API | `ClassicSubscriptionEngine` (the default for raw `Session`) |
| New code, options-based subscriptions, DI / Microsoft.Extensions.Options friendly | `DefaultSubscriptionEngine` (the default for `ManagedSession`) |
| Migrating a managed session but you still need a classic subscription you cannot rewrite | `ManagedSession` with `UseSubscriptionEngine(ClassicSubscriptionEngineFactory.Instance)`; `SubscriptionManager` then throws `InvalidOperationException` and you keep using `Subscriptions` |
| Migrating a managed session but you still need a classic subscription you cannot rewrite | `ManagedSession` with `UseSubscriptionEngine(ClassicSubscriptionEngineFactory.Instance)`; `TryGetSubscriptionManager` then returns `false` and you keep using `Subscriptions` |

The two engines can co-exist on a `ManagedSession`:
`ManagedSession.Subscriptions` (classic API) keeps working alongside
`ManagedSession.SubscriptionManager` (V2 API) when the V2 engine is
the V2 subscription manager obtained via
`ISession.TryGetSubscriptionManager(out var manager)` when the V2 engine is
active. Internally, classic subscriptions are bridged onto the V2
publish pipeline.

Expand Down
5 changes: 3 additions & 2 deletions Docs/Subscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,8 @@ The engine handles these as follows:

The V2 subscription engine
(`Opc.Ua.Client.Subscriptions.ISubscription`, returned from
`ManagedSession.SubscriptionManager.Add(...)`) lets a single logical
`session.TryGetSubscriptionManager(out var manager)` then `manager.Add(...)`)
lets a single logical
subscription hold an effectively unlimited number of monitored items.
When the per-subscription cap (`MaxMonitoredItemsPerSubscription` from
the server's capabilities, OPC UA Part 4 §5.13.2) would be exceeded,
Expand Down Expand Up @@ -835,7 +836,7 @@ reconciliation, etc.) along with the recommended V2 alternative.
|---|---|
| `Session.SubscriptionEngineFactory` defaulted to `ClassicSubscriptionEngineFactory.Instance` | Defaulted to `DefaultSubscriptionEngineFactory.Instance` on `ManagedSession`. Raw `Session` constructed with `DefaultSessionFactory` defaults to classic for backwards compatibility; opt in by passing `SubscriptionEngineFactory = DefaultSubscriptionEngineFactory.Instance`. |
| `Session.AddSubscription(Subscription)` (classic-typed) | Unchanged — classic subscriptions are still added via this API on classic-engine sessions. On a V2-engine `ManagedSession`, classic subscriptions are bridged onto the V2 publish pipeline by an internal `SubscriptionBridge`. |
| `ManagedSession.SubscriptionManager` (V2) | Available alongside `ManagedSession.Subscriptions` (classic API), so both surfaces coexist on the same session. |
| `ISession.TryGetSubscriptionManager` (V2) | Returns the V2 manager alongside `ManagedSession.Subscriptions` (classic API), so both surfaces coexist on the same session. |

### Per-item options mapping

Expand Down
17 changes: 14 additions & 3 deletions Docs/migrate/2.0.x/sessions-subscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,20 @@ ManagedSession session = await new ManagedSessionBuilder(configuration, telemetr

`Build()` returns an immutable `ManagedSessionOptions` snapshot; `ConnectAsync()` wraps `Build()` and `ManagedSession.CreateAsync(...)` so most callers can use the builder directly.

**New subscription API on `ManagedSession`:**

`ManagedSession` now exposes an `ISubscriptionManager` (the V2 options-based API) alongside the classic `Subscriptions` property. The V2 engine is the default for `ManagedSession`. Use `UseSubscriptionEngine(ClassicSubscriptionEngineFactory.Instance)` on the builder if you need the legacy classic engine instead — accessing `SubscriptionManager` then throws `InvalidOperationException`.
**New subscription API on `ISession`:**

The V2 options-based subscription manager (`ISubscriptionManager`) is exposed on `ISession` through `bool TryGetSubscriptionManager(out ISubscriptionManager? manager)` — it returns `true` and the manager for V2-engine sessions (the default for `ManagedSession`) and `false` for classic-engine sessions. The classic `Subscriptions` property remains available alongside it. Use `UseSubscriptionEngine(ClassicSubscriptionEngineFactory.Instance)` on the builder if you need the legacy classic engine instead.

> **Source-breaking (2.0 preview):** the earlier throwing `ManagedSession.SubscriptionManager` property has been **removed** in favor of `ISession.TryGetSubscriptionManager`. Replace `var manager = session.SubscriptionManager;` (which threw `InvalidOperationException` on classic-engine sessions) with:
>
> ```csharp
> if (session.TryGetSubscriptionManager(out ISubscriptionManager? manager))
> {
> // use manager (V2 engine)
> }
> ```
>
> The `session.AddSubscription(...)` fluent extensions are unchanged.

```csharp
using Opc.Ua.Client;
Expand Down
41 changes: 30 additions & 11 deletions Libraries/Opc.Ua.Client/Fluent/ManagedSessionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,29 @@ namespace Opc.Ua.Client
/// </summary>
public static class ManagedSessionExtensions
{
private static Subscriptions.ISubscriptionManager GetSubscriptionManager(
this ManagedSession session)
{
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}
if (!session.TryGetSubscriptionManager(
out Subscriptions.ISubscriptionManager? manager))
{
throw new InvalidOperationException(
"The managed session does not expose a V2 subscription manager. " +
"The session is using the classic engine; recreate the ManagedSession " +
"with the V2 subscription engine factory.");
}
return manager;
}
Comment thread
marcschier marked this conversation as resolved.

/// <summary>
/// Add a new subscription to the session using the supplied options
/// snapshot. The subscription is registered with
/// <see cref="ManagedSession.SubscriptionManager"/> and starts
/// snapshot. The subscription is registered with the session's V2
/// subscription manager (see
/// <see cref="ISession.TryGetSubscriptionManager"/>) and starts
/// asynchronously.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="session"/> is <c>null</c>.</exception>
Expand All @@ -72,7 +91,7 @@ public static ISubscription AddSubscription(
throw new ArgumentNullException(nameof(options));
}
var monitor = new OptionsMonitor<Subscriptions.SubscriptionOptions>(options);
return session.SubscriptionManager.Add(handler, monitor);
return session.GetSubscriptionManager().Add(handler, monitor);
}

/// <summary>
Expand Down Expand Up @@ -170,8 +189,8 @@ public static bool TryAddMonitoredItem(
/// portable across sessions whose tables index URIs in different
/// positions.
/// </summary>
/// <param name="session">Session whose
/// <see cref="ManagedSession.SubscriptionManager"/> is being
/// <param name="session">Session whose V2 subscription manager (see
/// <see cref="ISession.TryGetSubscriptionManager"/>) is being
/// snapshotted.</param>
/// <param name="destination">Writable destination stream.</param>
/// <param name="subscriptions">Optional subset of subscriptions
Expand All @@ -189,15 +208,15 @@ public static ValueTask SaveSubscriptionsAsync(
{
throw new ArgumentNullException(nameof(session));
}
return session.SubscriptionManager.SaveAsync(
return session.GetSubscriptionManager().SaveAsync(
destination, session.MessageContext, subscriptions, ct);
}

/// <summary>
/// Restore subscriptions previously persisted by
/// <see cref="SaveSubscriptionsAsync"/>. Each restored subscription is
/// re-registered with
/// <see cref="ManagedSession.SubscriptionManager"/>.
/// re-registered with the session's V2 subscription manager (see
/// <see cref="ISession.TryGetSubscriptionManager"/>).
/// </summary>
/// <param name="session">Session that owns the V2 subscription
/// manager and supplies the active message context.</param>
Expand All @@ -223,7 +242,7 @@ public static ValueTask<IReadOnlyList<ISubscription>> LoadSubscriptionsAsync(
{
throw new ArgumentNullException(nameof(session));
}
return session.SubscriptionManager.LoadAsync(source,
return session.GetSubscriptionManager().LoadAsync(source,
session.MessageContext, handlerFactory,
transferSubscriptions, ct);
}
Expand All @@ -246,7 +265,7 @@ public static IReadOnlyList<SubscriptionStateSnapshot> SnapshotSubscriptions(
throw new ArgumentNullException(nameof(session));
}
var result = new List<SubscriptionStateSnapshot>();
foreach (ISubscription s in session.SubscriptionManager.Items)
foreach (ISubscription s in session.GetSubscriptionManager().Items)
{
if (s is LogicalSubscription logical)
{
Expand Down Expand Up @@ -308,7 +327,7 @@ public static async ValueTask<IReadOnlyList<ISubscription>> RestoreSubscriptions
}
var result = new List<ISubscription>(states.Count);
var manager =
(SubscriptionManager)session.SubscriptionManager;
(SubscriptionManager)session.GetSubscriptionManager();

// Group by LogicalGroupId; null group = standalone. The
// grouping logic mirrors the stream-based LoadAsync
Expand Down
20 changes: 20 additions & 0 deletions Libraries/Opc.Ua.Client/Session/ISession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -379,6 +380,25 @@ IEnumerable<Subscription> Load(
bool transferSubscriptions = false,
IEnumerable<Type>? knownTypes = null);

/// <summary>
/// Gets the options-based V2 subscription manager for this session.
/// The manager is available when the session was created with the V2
/// subscription engine (the default for
/// <see cref="ManagedSession"/>). Sessions using the classic
/// subscription engine do not expose a manager.
/// </summary>
/// <param name="manager">
/// When this method returns <c>true</c>, contains the session's
/// <see cref="Subscriptions.ISubscriptionManager"/>; otherwise
/// <c>null</c>.
/// </param>
/// <returns>
/// <c>true</c> if the session uses the V2 subscription engine and a
/// manager is available; otherwise <c>false</c>.
/// </returns>
bool TryGetSubscriptionManager(
[NotNullWhen(true)] out Subscriptions.ISubscriptionManager? manager);

/// <summary>
/// Returns the active session configuration and writes it to a stream.
/// </summary>
Expand Down
13 changes: 11 additions & 2 deletions Libraries/Opc.Ua.Client/Session/ManagedSession.Streaming.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ public partial class ManagedSession

/// <summary>
/// A shared lazy <see cref="IStreamingSubscription"/> over this
/// session's <see cref="SubscriptionManager"/>. Backs both
/// session's V2 subscription manager (see
/// <see cref="ISession.TryGetSubscriptionManager"/>). Backs both
/// <see cref="ModelChange"/> (when enabled) and ad-hoc
/// <see cref="IAsyncEnumerable{T}"/>-based subscriptions.
/// </summary>
Expand All @@ -78,7 +79,15 @@ public IStreamingSubscription DefaultStreaming
return m_defaultStreaming;
}

m_defaultStreaming = new StreamingSubscription(SubscriptionManager);
if (!TryGetSubscriptionManager(
out Subscriptions.ISubscriptionManager? manager))
{
throw new InvalidOperationException(
"Streaming subscriptions require the V2 subscription engine. " +
"The session is using the classic engine; recreate the " +
"ManagedSession with the V2 subscription engine factory.");
}
m_defaultStreaming = new StreamingSubscription(manager);
return m_defaultStreaming;
}
}
Expand Down
27 changes: 7 additions & 20 deletions Libraries/Opc.Ua.Client/Session/ManagedSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ private ManagedSession(
/// the server certificate.</param>
/// <param name="engineFactory">Optional subscription engine
/// factory. Defaults to <see cref="DefaultSubscriptionEngineFactory"/>
/// (V2 engine) so that <see cref="SubscriptionManager"/> is
/// (V2 engine) so that the V2 subscription manager (see
/// <see cref="ISession.TryGetSubscriptionManager"/>) is
/// available. Pass <see cref="ClassicSubscriptionEngineFactory"/>
/// for legacy classic-engine behavior.</param>
/// <param name="transferSubscriptionsOnRecreate">When
Expand Down Expand Up @@ -348,26 +349,12 @@ public ArrayOf<string> PreferredLocales
public IEnumerable<Subscription> Subscriptions
=> m_session?.Subscriptions ?? [];

/// <summary>
/// The new options-based <see cref="Subscriptions.ISubscriptionManager"/>.
/// Available when the underlying session was created with the V2
/// subscription engine (the default for <see cref="ManagedSession"/>).
/// </summary>
/// <exception cref="InvalidOperationException">when the session
/// is using the classic engine.</exception>
public Subscriptions.ISubscriptionManager SubscriptionManager
/// <inheritdoc/>
public bool TryGetSubscriptionManager(
[System.Diagnostics.CodeAnalysis.NotNullWhen(true)]
out Subscriptions.ISubscriptionManager? manager)
{
get
{
if (InnerSession.SubscriptionEngine is DefaultSubscriptionEngine v2)
{
return v2.SubscriptionManager;
}
throw new InvalidOperationException(
"ManagedSession.SubscriptionManager requires the V2 subscription engine. " +
"The session is using the classic engine; use Subscriptions/AddSubscription " +
"for the legacy API or recreate the ManagedSession with the V2 engine factory.");
}
return InnerSession.TryGetSubscriptionManager(out manager);
}

/// <inheritdoc/>
Expand Down
5 changes: 3 additions & 2 deletions Libraries/Opc.Ua.Client/Session/ManagedSessionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ public sealed record ManagedSessionOptions

/// <summary>
/// Optional subscription engine factory. When null, defaults to the
/// V2 engine (<see cref="DefaultSubscriptionEngineFactory"/>) so
/// <see cref="ManagedSession.SubscriptionManager"/> is available.
/// V2 engine (<see cref="DefaultSubscriptionEngineFactory"/>) so the
/// session's V2 subscription manager (see
/// <see cref="ISession.TryGetSubscriptionManager"/>) is available.
/// </summary>
public ISubscriptionEngineFactory? SubscriptionEngineFactory { get; init; }

Expand Down
14 changes: 14 additions & 0 deletions Libraries/Opc.Ua.Client/Session/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,20 @@ public event EventHandler SessionConfigurationChanged
/// </summary>
internal ISubscriptionEngine SubscriptionEngine => m_engine;

/// <inheritdoc/>
public bool TryGetSubscriptionManager(
[System.Diagnostics.CodeAnalysis.NotNullWhen(true)]
out Subscriptions.ISubscriptionManager? manager)
{
if (m_engine is DefaultSubscriptionEngine v2)
{
manager = v2.SubscriptionManager;
return true;
}
manager = null;
return false;
}

/// <summary>
/// Gets the endpoint used to connect to the server.
/// </summary>
Expand Down
Loading
Loading