mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
Add typed IVariableBuilder<T> fluent surface
Adds typed sub-interfaces and impls so the existing fluent surface can expose simple Func<T> / Action<T> / Func<CancellationToken, ValueTask<T>> overloads for variables — without losing the lower-level Variant-based overloads on INodeBuilder. * IVariableBuilder<TValue> + VariableBuilder<TValue> derive from INodeBuilder<BaseVariableState> / NodeBuilder<BaseVariableState> (NodeBuilder<TState> unsealed). * Marshalling uses Variant.AsBoxedObject(BoxingBehavior.Legacy) for reads and Variant(object) for writes; the AOT-unsafe write path is scoped-suppressed with a TODO pointing at the planned per-type generated walker. * INodeManagerBuilder gains Variable<T>(string), Variable<T>(NodeId), VariableFromTypeId<T>(NodeId), VariableFromTypeId<T>(NodeId, QualifiedName). Server library builds clean across all 6 TFMs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -198,5 +198,43 @@ namespace Opc.Ua.Server.Fluent
|
||||
/// <typeparam name="TState">See <see cref="NodeFromTypeId{TState}(NodeId)"/>.</typeparam>
|
||||
INodeBuilder<TState> NodeFromTypeId<TState>(NodeId typeDefinitionId, QualifiedName browseName)
|
||||
where TState : NodeState;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a variable node by browse path and returns a typed
|
||||
/// <see cref="IVariableBuilder{TValue}"/> view that exposes
|
||||
/// simple <c>Func</c> / <c>Action</c> shaped
|
||||
/// <c>OnRead</c>/<c>OnWrite</c> overloads.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">
|
||||
/// CLR type carried by the variable's <c>Value</c> attribute.
|
||||
/// </typeparam>
|
||||
/// <param name="browsePath">See <see cref="Node(string)"/>.</param>
|
||||
/// <exception cref="ServiceResultException">
|
||||
/// Thrown if the path does not resolve, or resolves to a node
|
||||
/// that is not a <see cref="BaseVariableState"/>.
|
||||
/// </exception>
|
||||
IVariableBuilder<TValue> Variable<TValue>(string browsePath);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a variable node by absolute <see cref="NodeId"/>
|
||||
/// and returns a typed <see cref="IVariableBuilder{TValue}"/>
|
||||
/// view.
|
||||
/// </summary>
|
||||
IVariableBuilder<TValue> Variable<TValue>(NodeId nodeId);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the unique variable instance whose
|
||||
/// <c>TypeDefinitionId</c> matches <paramref name="typeDefinitionId"/>
|
||||
/// and returns a typed <see cref="IVariableBuilder{TValue}"/>
|
||||
/// view. Same disambiguation semantics as
|
||||
/// <see cref="NodeFromTypeId(NodeId)"/>.
|
||||
/// </summary>
|
||||
IVariableBuilder<TValue> VariableFromTypeId<TValue>(NodeId typeDefinitionId);
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="VariableFromTypeId{TValue}(NodeId)"/> but
|
||||
/// disambiguates among multiple instances by browse name.
|
||||
/// </summary>
|
||||
IVariableBuilder<TValue> VariableFromTypeId<TValue>(NodeId typeDefinitionId, QualifiedName browseName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/* ========================================================================
|
||||
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
||||
*
|
||||
* OPC Foundation MIT License 1.00
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* The complete license agreement can be found here:
|
||||
* http://opcfoundation.org/License/MIT/1.00/
|
||||
* ======================================================================*/
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Opc.Ua.Server.Fluent
|
||||
{
|
||||
/// <summary>
|
||||
/// Strongly-typed view of a variable node whose value attribute is
|
||||
/// known at compile time to carry <typeparamref name="TValue"/>.
|
||||
/// Surfaces convenience overloads of <c>OnRead</c>/<c>OnWrite</c>
|
||||
/// that accept simple <see cref="Func{TResult}"/> /
|
||||
/// <see cref="Action{T}"/> shaped lambdas — the framework marshals
|
||||
/// to and from <see cref="Variant"/> automatically.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Obtained via
|
||||
/// <see cref="INodeManagerBuilder.Variable{TValue}(string)"/>,
|
||||
/// <see cref="INodeManagerBuilder.Variable{TValue}(NodeId)"/> or by
|
||||
/// calling <see cref="VariableBuilderExtensions.AsVariable{TValue}(INodeBuilder)"/>
|
||||
/// on an existing <see cref="INodeBuilder"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The simple convenience overloads do not surface index range, data
|
||||
/// encoding, or per-call status code information; use the underlying
|
||||
/// <see cref="INodeBuilder"/> overloads (e.g.
|
||||
/// <see cref="INodeBuilder.OnRead(NodeValueEventHandlerAsync)"/>) when
|
||||
/// that level of control is needed.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <typeparam name="TValue">
|
||||
/// CLR type that the variable's <c>Value</c> attribute is expected to
|
||||
/// carry. Reads cast the underlying <see cref="Variant"/> to
|
||||
/// this type; writes wrap the supplied value in a new
|
||||
/// <see cref="Variant"/>.
|
||||
/// </typeparam>
|
||||
public interface IVariableBuilder<TValue> : INodeBuilder<BaseVariableState>
|
||||
{
|
||||
/// <summary>
|
||||
/// Wires a synchronous typed getter. The framework invokes
|
||||
/// <paramref name="getter"/> on every read and applies index range
|
||||
/// / data encoding / copy policy post-processing to the returned
|
||||
/// value.
|
||||
/// </summary>
|
||||
IVariableBuilder<TValue> OnRead(Func<TValue> getter);
|
||||
|
||||
/// <summary>
|
||||
/// Wires a synchronous typed getter that receives the calling
|
||||
/// <see cref="ISystemContext"/>.
|
||||
/// </summary>
|
||||
IVariableBuilder<TValue> OnRead(Func<ISystemContext, TValue> getter);
|
||||
|
||||
/// <summary>
|
||||
/// Wires an asynchronous typed getter. The handler runs without
|
||||
/// holding <c>lock(this)</c>; the framework still applies index
|
||||
/// range / data encoding / copy policy post-processing.
|
||||
/// </summary>
|
||||
IVariableBuilder<TValue> OnRead(
|
||||
Func<CancellationToken, ValueTask<TValue>> getter);
|
||||
|
||||
/// <summary>
|
||||
/// Wires an asynchronous typed getter that receives the calling
|
||||
/// <see cref="ISystemContext"/> and a <see cref="CancellationToken"/>.
|
||||
/// </summary>
|
||||
IVariableBuilder<TValue> OnRead(
|
||||
Func<ISystemContext, CancellationToken, ValueTask<TValue>> getter);
|
||||
|
||||
/// <summary>
|
||||
/// Wires a synchronous typed setter. Index-range writes are not
|
||||
/// supported through this overload (use the
|
||||
/// <see cref="NodeValueEventHandler"/> overload on
|
||||
/// <see cref="INodeBuilder.OnWrite(NodeValueEventHandler)"/>).
|
||||
/// </summary>
|
||||
IVariableBuilder<TValue> OnWrite(Action<TValue> setter);
|
||||
|
||||
/// <summary>
|
||||
/// Wires a synchronous typed setter that receives the calling
|
||||
/// <see cref="ISystemContext"/>.
|
||||
/// </summary>
|
||||
IVariableBuilder<TValue> OnWrite(Action<ISystemContext, TValue> setter);
|
||||
|
||||
/// <summary>
|
||||
/// Wires an asynchronous typed setter. Runs without holding
|
||||
/// <c>lock(this)</c>.
|
||||
/// </summary>
|
||||
IVariableBuilder<TValue> OnWrite(
|
||||
Func<TValue, CancellationToken, ValueTask> setter);
|
||||
|
||||
/// <summary>
|
||||
/// Wires an asynchronous typed setter that receives the calling
|
||||
/// <see cref="ISystemContext"/>.
|
||||
/// </summary>
|
||||
IVariableBuilder<TValue> OnWrite(
|
||||
Func<ISystemContext, TValue, CancellationToken, ValueTask> setter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience extensions that bridge a non-generic
|
||||
/// <see cref="INodeBuilder"/> into the strongly-typed
|
||||
/// <see cref="IVariableBuilder{TValue}"/> view.
|
||||
/// </summary>
|
||||
public static class VariableBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a <see cref="IVariableBuilder{TValue}"/> view of the
|
||||
/// resolved node. Throws if the node is not a
|
||||
/// <see cref="BaseVariableState"/>.
|
||||
/// </summary>
|
||||
public static IVariableBuilder<TValue> AsVariable<TValue>(this INodeBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (builder.Node is not BaseVariableState variable)
|
||||
{
|
||||
throw ServiceResultException.Create(
|
||||
StatusCodes.BadInvalidArgument,
|
||||
"Cannot get a typed variable view of node '{0}': not a BaseVariableState (actual: {1}).",
|
||||
builder.Node.BrowseName,
|
||||
builder.Node.GetType().Name);
|
||||
}
|
||||
|
||||
return new VariableBuilder<TValue>(builder.Builder, variable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,7 +293,7 @@ namespace Opc.Ua.Server.Fluent
|
||||
/// <see cref="Node"/> property.
|
||||
/// </summary>
|
||||
/// <typeparam name="TState"></typeparam>
|
||||
internal sealed class NodeBuilder<TState> : NodeBuilder, INodeBuilder<TState>
|
||||
internal class NodeBuilder<TState> : NodeBuilder, INodeBuilder<TState>
|
||||
where TState : NodeState
|
||||
{
|
||||
public NodeBuilder(NodeManagerBuilder parent, TState node)
|
||||
|
||||
@@ -237,6 +237,68 @@ namespace Opc.Ua.Server.Fluent
|
||||
return new NodeBuilder<TState>(this, typed);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IVariableBuilder<TValue> Variable<TValue>(string browsePath)
|
||||
{
|
||||
ThrowIfSealed();
|
||||
NodeState node = BrowsePathResolver.Resolve(
|
||||
Context,
|
||||
browsePath,
|
||||
m_defaultNamespaceIndex,
|
||||
m_rootResolver);
|
||||
return ToVariableBuilder<TValue>(node, browsePath);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IVariableBuilder<TValue> Variable<TValue>(NodeId nodeId)
|
||||
{
|
||||
ThrowIfSealed();
|
||||
NodeState node = ResolveNodeId(nodeId);
|
||||
return ToVariableBuilder<TValue>(node, FormatNodeId(nodeId));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IVariableBuilder<TValue> VariableFromTypeId<TValue>(NodeId typeDefinitionId)
|
||||
{
|
||||
ThrowIfSealed();
|
||||
NodeState node = ResolveByTypeDefinition(typeDefinitionId, (QualifiedName)null);
|
||||
return ToVariableBuilder<TValue>(node, FormatNodeId(typeDefinitionId));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IVariableBuilder<TValue> VariableFromTypeId<TValue>(NodeId typeDefinitionId, QualifiedName browseName)
|
||||
{
|
||||
ThrowIfSealed();
|
||||
NodeState node = ResolveByTypeDefinition(typeDefinitionId, browseName);
|
||||
return ToVariableBuilder<TValue>(
|
||||
node,
|
||||
CoreUtils.Format(
|
||||
"{0} (browse name '{1}')",
|
||||
FormatNodeId(typeDefinitionId),
|
||||
browseName));
|
||||
}
|
||||
|
||||
private VariableBuilder<TValue> ToVariableBuilder<TValue>(NodeState node, string lookupHint)
|
||||
{
|
||||
if (node is not BaseVariableState variable)
|
||||
{
|
||||
throw ServiceResultException.Create(
|
||||
StatusCodes.BadTypeMismatch,
|
||||
"Lookup '{0}' resolved to {1}, which is not a BaseVariableState.",
|
||||
lookupHint,
|
||||
node.GetType().Name);
|
||||
}
|
||||
return new VariableBuilder<TValue>(this, variable);
|
||||
}
|
||||
|
||||
private static string FormatNodeId(NodeId nodeId)
|
||||
{
|
||||
// NodeId is a readonly struct so the caller may pass `default`;
|
||||
// .IsNull guards both the default-struct case and a constructed
|
||||
// NodeId with no identifier.
|
||||
return nodeId.IsNull ? "(null)" : nodeId.ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryHandleHistoryRead(
|
||||
ISystemContext context,
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/* ========================================================================
|
||||
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
||||
*
|
||||
* OPC Foundation MIT License 1.00
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* The complete license agreement can be found here:
|
||||
* http://opcfoundation.org/License/MIT/1.00/
|
||||
* ======================================================================*/
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Opc.Ua.Server.Fluent
|
||||
{
|
||||
/// <summary>
|
||||
/// Default <see cref="IVariableBuilder{TValue}"/> implementation.
|
||||
/// Wraps each typed convenience overload as the appropriate slot
|
||||
/// delegate on the underlying <see cref="BaseVariableState"/> and
|
||||
/// performs <see cref="Variant"/> marshalling on every read / write.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
internal sealed class VariableBuilder<TValue> :
|
||||
NodeBuilder<BaseVariableState>,
|
||||
IVariableBuilder<TValue>
|
||||
{
|
||||
public VariableBuilder(INodeManagerBuilder parent, BaseVariableState variable)
|
||||
: base((NodeManagerBuilder)parent, variable)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IVariableBuilder<TValue> OnRead(Func<TValue> getter)
|
||||
{
|
||||
if (getter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(getter));
|
||||
}
|
||||
OnRead((ISystemContext _) => getter());
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IVariableBuilder<TValue> OnRead(Func<ISystemContext, TValue> getter)
|
||||
{
|
||||
if (getter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(getter));
|
||||
}
|
||||
((INodeBuilder)this).OnRead(
|
||||
(ISystemContext context, NodeState _, ref Variant value) =>
|
||||
{
|
||||
value = ToVariant(getter(context));
|
||||
return ServiceResult.Good;
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IVariableBuilder<TValue> OnRead(
|
||||
Func<CancellationToken, ValueTask<TValue>> getter)
|
||||
{
|
||||
if (getter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(getter));
|
||||
}
|
||||
return OnRead(
|
||||
(ISystemContext _, CancellationToken ct) => getter(ct));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IVariableBuilder<TValue> OnRead(
|
||||
Func<ISystemContext, CancellationToken, ValueTask<TValue>> getter)
|
||||
{
|
||||
if (getter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(getter));
|
||||
}
|
||||
((INodeBuilder)this).OnRead(
|
||||
async (ISystemContext context, NodeState _, CancellationToken ct) =>
|
||||
{
|
||||
TValue typed = await getter(context, ct).ConfigureAwait(false);
|
||||
return new AttributeSimpleReadResult(
|
||||
ServiceResult.Good,
|
||||
ToVariant(typed));
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IVariableBuilder<TValue> OnWrite(Action<TValue> setter)
|
||||
{
|
||||
if (setter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(setter));
|
||||
}
|
||||
OnWrite((ISystemContext _, TValue v) => setter(v));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IVariableBuilder<TValue> OnWrite(Action<ISystemContext, TValue> setter)
|
||||
{
|
||||
if (setter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(setter));
|
||||
}
|
||||
((INodeBuilder)this).OnWrite(
|
||||
(ISystemContext context, NodeState _, ref Variant value) =>
|
||||
{
|
||||
setter(context, FromVariant(value));
|
||||
return ServiceResult.Good;
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IVariableBuilder<TValue> OnWrite(
|
||||
Func<TValue, CancellationToken, ValueTask> setter)
|
||||
{
|
||||
if (setter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(setter));
|
||||
}
|
||||
return OnWrite((ISystemContext _, TValue v, CancellationToken ct) => setter(v, ct));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IVariableBuilder<TValue> OnWrite(
|
||||
Func<ISystemContext, TValue, CancellationToken, ValueTask> setter)
|
||||
{
|
||||
if (setter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(setter));
|
||||
}
|
||||
((INodeBuilder)this).OnWrite(
|
||||
async (ISystemContext context, NodeState _, Variant value, CancellationToken ct) =>
|
||||
{
|
||||
TValue typed = FromVariant(value);
|
||||
await setter(context, typed, ct).ConfigureAwait(false);
|
||||
return new AttributeWriteResult(ServiceResult.Good);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Casts the boxed <see cref="Variant"/> to
|
||||
/// <typeparamref name="TValue"/>. Returns the default value when
|
||||
/// the variant is null so a typed lambda never has to defend
|
||||
/// against an empty variable.
|
||||
/// </summary>
|
||||
private static TValue FromVariant(Variant value)
|
||||
{
|
||||
object boxed = value.AsBoxedObject(Variant.BoxingBehavior.Legacy);
|
||||
if (boxed is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
if (boxed is TValue typed)
|
||||
{
|
||||
return typed;
|
||||
}
|
||||
// Fall through to checked cast — gives a clear InvalidCastException
|
||||
// (vs. a confusing pattern-match miss) when the model and the user
|
||||
// disagree about the variable type.
|
||||
return (TValue)boxed;
|
||||
}
|
||||
|
||||
// The reflection-based Variant(object) constructor is the only
|
||||
// generic entry point for an open-ended TValue. AOT users should
|
||||
// prefer the per-type generated walker (FluentBuilderGenerator) which
|
||||
// routes through the typed Variant.From overloads instead. The
|
||||
// suppression is scoped to this single call site so trim/AOT
|
||||
// analysis still tracks every other path through this assembly.
|
||||
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage(
|
||||
"Trimming",
|
||||
"IL2026:RequiresUnreferencedCode",
|
||||
Justification = "Generic typed-variable bridge requires the reflection-based Variant(object) constructor.")]
|
||||
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage(
|
||||
"AOT",
|
||||
"IL3050:RequiresDynamicCode",
|
||||
Justification = "Generic typed-variable bridge requires the reflection-based Variant(object) constructor.")]
|
||||
private static Variant ToVariant(TValue value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return Variant.Null;
|
||||
}
|
||||
#pragma warning disable CS0618 // Variant(object) is obsolete on net472
|
||||
return new Variant(value);
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user