< Summary

Information
Class: IceRpc.ServiceAddress
Assembly: IceRpc
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/ServiceAddress.cs
Tag: 275_13775359185
Line coverage
96%
Covered lines: 308
Uncovered lines: 10
Coverable lines: 318
Total lines: 577
Line coverage: 96.8%
Branch coverage
96%
Covered branches: 128
Total branches: 132
Branch coverage: 96.9%
Method coverage
100%
Covered methods: 27
Total methods: 27
Method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Protocol()100%11100%
get_ServerAddress()100%11100%
set_ServerAddress(...)100%1414100%
get_Path()100%11100%
set_Path(...)100%22100%
get_AltServerAddresses()100%11100%
set_AltServerAddresses(...)87.5%8.22885%
get_Params()100%11100%
set_Params(...)83.33%6.05688.88%
get_Fragment()100%11100%
set_Fragment(...)100%66100%
get_OriginalUri()100%11100%
.cctor()100%11100%
.ctor(...)100%11100%
.ctor(...)100%26.042696.15%
Equals(...)90%20.472089.47%
GetHashCode()100%22100%
ToString()100%2020100%
StartQueryOption()100%22100%
ToUri()100%44100%
CheckParams(...)100%66100%
CheckPath(...)100%66100%
IsValidParamValue(...)100%11100%
.ctor(...)100%11100%
CheckFragment(...)100%22100%
IsValid(...)100%22100%
IsValidParamName(...)100%66100%

File(s)

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/ServiceAddress.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using IceRpc.Internal;
 4using System.Buffers;
 5using System.Collections.Immutable;
 6using System.ComponentModel;
 7using System.Diagnostics;
 8using System.Globalization;
 9using System.Text;
 10
 11namespace IceRpc;
 12
 13/// <summary>Represents the URI of a service, parsed and processed for easier consumption by invokers. It's used to
 14/// construct an <see cref="OutgoingRequest" />.</summary>
 15// The properties of this class are sorted in URI order.
 16[TypeConverter(typeof(ServiceAddressTypeConverter))]
 17public sealed record class ServiceAddress
 18{
 19    /// <summary>Gets the protocol of this service address.</summary>
 20    /// <value>The protocol of the service address. It corresponds to the URI scheme and is <see langword="null" /> for
 21    /// a relative service address.</value>
 1064222    public Protocol? Protocol { get; }
 23
 24    /// <summary>Gets or initializes the main server address of this service address.</summary>
 25    /// <value>The main server address of this service address, or <see langword="null"/> if this service address has no
 26    /// server address.</value>
 27    public ServerAddress? ServerAddress
 28    {
 86729        get => _serverAddress;
 30
 31        init
 3432        {
 3433            if (Protocol is null)
 234            {
 235                throw new InvalidOperationException(
 236                    $"Cannot set {nameof(ServerAddress)} on a relative service address.");
 37            }
 38
 3239            if (value?.Protocol is Protocol newProtocol && newProtocol != Protocol)
 240            {
 241                throw new ArgumentException(
 242                    $"The {nameof(ServerAddress)} must use the service address's protocol: '{Protocol}'.",
 243                    nameof(value));
 44            }
 45
 3046            if (value is not null)
 2847            {
 2848                if (_params.Count > 0)
 249                {
 250                    throw new InvalidOperationException(
 251                        $"Cannot set {nameof(ServerAddress)} on a service address with parameters.");
 52                }
 2653            }
 254            else if (_altServerAddresses.Count > 0)
 255            {
 256                throw new InvalidOperationException(
 257                    $"Cannot clear {nameof(ServerAddress)} when {nameof(AltServerAddresses)} is not empty.");
 58            }
 2659            _serverAddress = value;
 2660            OriginalUri = null;
 2661        }
 62    }
 63
 64    /// <summary>Gets or initializes the path of this service address.</summary>
 65    /// <value>The path of this service address. Defaults to <c>/</c>.</value>
 66    public string Path
 67    {
 659868        get => _path;
 69        init
 8470        {
 71            try
 8472            {
 8473                CheckPath(value); // make sure it's properly escaped
 8274                Protocol?.CheckPath(value); // make sure the protocol is happy with this path
 8075            }
 476            catch (FormatException exception)
 477            {
 478                throw new ArgumentException("Invalid path.", nameof(value), exception);
 79            }
 8080            _path = value;
 8081            OriginalUri = null;
 8082        }
 83    }
 84
 85    /// <summary>Gets or initializes the secondary server addresses of this service address.</summary>
 86    /// <value>The secondary server addresses of this service address. Defaults to <see cref="ImmutableList{T}.Empty"
 87    /// />.</value>
 88    public ImmutableList<ServerAddress> AltServerAddresses
 89    {
 75890        get => _altServerAddresses;
 91
 92        init
 1293        {
 1294            if (Protocol is null)
 095            {
 096                throw new InvalidOperationException(
 097                    $"Cannot set {nameof(AltServerAddresses)} on a relative service address.");
 98            }
 99
 12100            if (value.Count > 0)
 12101            {
 12102                if (_serverAddress is null)
 2103                {
 2104                    throw new InvalidOperationException(
 2105                        $"Cannot set {nameof(AltServerAddresses)} when {nameof(ServerAddress)} is empty.");
 106                }
 107
 26108                if (value.Any(e => e.Protocol != Protocol))
 2109                {
 2110                    throw new ArgumentException(
 2111                        $"The {nameof(AltServerAddresses)} server addresses must use the service address's protocol: '{P
 2112                        nameof(value));
 113                }
 8114            }
 115            // else, no need to check anything, an empty list is always fine.
 116
 8117            _altServerAddresses = value;
 8118            OriginalUri = null;
 8119        }
 120    }
 121
 122    /// <summary>Gets or initializes the parameters of this service address.</summary>
 123    /// <value>The params dictionary. Always empty if <see cref="ServerAddress" /> is not <see langword="null"/>.
 124    /// Defaults to <see cref="ImmutableDictionary{TKey, TValue}.Empty" />.</value>.
 125    public ImmutableDictionary<string, string> Params
 126    {
 519127        get => _params;
 128        init
 106129        {
 106130            if (Protocol is null)
 0131            {
 0132                throw new InvalidOperationException($"Cannot set {nameof(Params)} on a relative service address.");
 133            }
 134
 135            try
 106136            {
 106137                CheckParams(value); // general checking (properly escape, no empty name)
 106138                Protocol.CheckServiceAddressParams(value); // protocol-specific checking
 104139            }
 2140            catch (FormatException exception)
 2141            {
 2142                throw new ArgumentException("Invalid parameters.", nameof(value), exception);
 143            }
 144
 104145            if (_serverAddress is not null && value.Count > 0)
 2146            {
 2147                throw new InvalidOperationException(
 2148                    $"Cannot set {nameof(Params)} on a service address with a serverAddress.");
 149            }
 150
 102151            _params = value;
 102152            OriginalUri = null;
 102153        }
 154    }
 155
 156    /// <summary>Gets or initializes the fragment.</summary>
 157    /// <value>The fragment of this service address. Defaults to an empty string.</value>
 158    public string Fragment
 159    {
 6444160        get => _fragment;
 161        init
 8162        {
 8163            if (Protocol is null)
 2164            {
 2165                throw new InvalidOperationException($"Cannot set {nameof(Fragment)} on a relative service address.");
 166            }
 167
 168            try
 6169            {
 6170                CheckFragment(value); // make sure it's properly escaped
 4171            }
 2172            catch (FormatException exception)
 2173            {
 2174                throw new ArgumentException("Invalid fragment.", nameof(value), exception);
 175            }
 176
 4177            if (!Protocol.HasFragment && value.Length > 0)
 2178            {
 2179                throw new InvalidOperationException($"Cannot set {Fragment} on an {Protocol} service address.");
 180            }
 181
 2182            _fragment = value;
 2183            OriginalUri = null;
 2184        }
 185    }
 186
 187    /// <summary>Gets the URI used to create this service address.</summary>
 188    /// <value>The <see cref="Uri" /> of this service address if it was constructed from a URI and if URI-derived
 189    /// properties have not been updated. The setting of a URI-derived property such as <see cref="ServerAddress" />
 190    /// sets <see cref="OriginalUri" /> to <see langword="null"/>.</value>
 1367191    public Uri? OriginalUri { get; private set; }
 192
 193    // The printable ASCII character range is x20 (space) to x7E inclusive. Space is an invalid character in path,
 194    // fragment, etc. in addition to the invalid characters in the _notValidInXXX search values.
 195    private const char FirstValidChar = '\x21';
 196    private const char LastValidChar = '\x7E';
 197
 14198    private static readonly SearchValues<char> _notValidInFragment = SearchValues.Create("\"<>\\^`{|}");
 14199    private static readonly SearchValues<char> _notValidInParamName = SearchValues.Create("\"<>#&=\\^`{|}");
 14200    private static readonly SearchValues<char> _notValidInParamValue = SearchValues.Create("\"<>#&\\^`{|}");
 14201    private static readonly SearchValues<char> _notValidInPath = SearchValues.Create("\"<>#?\\^`{|}");
 202
 6376203    private ImmutableList<ServerAddress> _altServerAddresses = ImmutableList<ServerAddress>.Empty;
 6376204    private string _fragment = "";
 6376205    private ImmutableDictionary<string, string> _params = ImmutableDictionary<string, string>.Empty;
 6376206    private string _path = "/";
 207    private ServerAddress? _serverAddress;
 208
 209    /// <summary>Constructs a service address from a protocol.</summary>
 210    /// <param name="protocol">The protocol, or <see langword="null" /> for a relative service address.</param>
 11178211    public ServiceAddress(Protocol? protocol = null) => Protocol = protocol;
 212
 213    /// <summary>Constructs a service address from a URI.</summary>
 214    /// <param name="uri">The Uri.</param>
 750215    public ServiceAddress(Uri uri)
 750216    {
 750217        if (uri.IsAbsoluteUri)
 716218        {
 716219            Protocol = Protocol.TryParse(uri.Scheme, out Protocol? protocol) ? protocol :
 716220                throw new ArgumentException(
 716221                    $"Cannot create a service address with protocol '{uri.Scheme}'.",
 716222                    nameof(uri));
 223
 224            // The AbsolutePath is empty for a URI such as "icerpc:?foo=bar"
 714225            _path = uri.AbsolutePath.Length > 0 ? uri.AbsolutePath : "/";
 714226            _fragment = uri.Fragment.Length > 0 ? uri.Fragment[1..] : ""; // remove leading #
 227
 228            try
 714229            {
 714230                Protocol.CheckPath(_path);
 712231            }
 2232            catch (FormatException exception)
 2233            {
 2234                throw new ArgumentException($"Invalid path in {Protocol} URI.", nameof(uri), exception);
 235            }
 236
 712237            if (!Protocol.HasFragment && _fragment.Length > 0)
 4238            {
 4239                throw new ArgumentException(
 4240                    $"Cannot create an {Protocol} service address with a fragment.",
 4241                    nameof(uri));
 242            }
 243
 708244            (ImmutableDictionary<string, string> queryParams, string? altServerValue, string? transport) =
 708245                uri.ParseQuery();
 246
 708247            if (uri.Authority.Length > 0)
 603248            {
 603249                if (uri.UserInfo.Length > 0)
 2250                {
 2251                    throw new ArgumentException("Cannot create a server address with a user info.", nameof(uri));
 252                }
 253
 601254                string host = uri.IdnHost;
 601255                Debug.Assert(host.Length > 0); // the IdnHost provided by Uri is never empty
 256
 601257                _serverAddress = new ServerAddress(
 601258                    Protocol,
 601259                    host,
 601260                    port: uri.Port == -1 ? Protocol.DefaultPort : checked((ushort)uri.Port),
 601261                    transport,
 601262                    queryParams);
 263
 601264                if (altServerValue is not null)
 83265                {
 266                    // Split and parse recursively each serverAddress
 569267                    foreach (string serverAddressStr in altServerValue.Split(','))
 163268                    {
 163269                        string altUriString = $"{uri.Scheme}://{serverAddressStr}";
 270
 271                        // The separator for server address parameters in alt-server is $, so we replace these '$'
 272                        // by '&' before sending the string (Uri) to the ServerAddress constructor which uses '&' as
 273                        // separator.
 163274                        _altServerAddresses = _altServerAddresses.Add(
 163275                            new ServerAddress(new Uri(altUriString.Replace('$', '&'))));
 157276                    }
 77277                }
 595278            }
 279            else
 105280            {
 105281                if (!_path.StartsWith('/'))
 2282                {
 2283                    throw new ArgumentException(
 2284                        $"Invalid path in service address URI '{uri.OriginalString}'.",
 2285                        nameof(uri));
 286                }
 287
 103288                if (altServerValue is not null)
 2289                {
 2290                    throw new ArgumentException(
 2291                        $"Invalid alt-server parameter in URI '{uri.OriginalString}'.",
 2292                        nameof(uri));
 293                }
 294
 295                try
 101296                {
 101297                    Protocol.CheckServiceAddressParams(queryParams);
 97298                }
 4299                catch (FormatException exception)
 4300                {
 4301                    throw new ArgumentException("Invalid parameters in URI.", nameof(uri), exception);
 302                }
 303
 97304                Params = queryParams;
 97305            }
 692306        }
 307        else
 34308        {
 309            // relative service address
 34310            Protocol = null;
 34311            _path = uri.ToString();
 312
 313            try
 34314            {
 34315                CheckPath(_path);
 34316            }
 0317            catch (FormatException exception)
 0318            {
 0319                throw new ArgumentException("Invalid path in relative URI.", nameof(uri), exception);
 320            }
 34321        }
 322
 726323        OriginalUri = uri;
 726324    }
 325
 326    /// <summary>Determines whether the specified <see cref="ServiceAddress"/> is equal to the current
 327    /// <see cref="ServiceAddress"/>.</summary>
 328    /// <param name="other">The <see cref="ServiceAddress"/> to compare with the current <see cref="ServiceAddress"/>.
 329    /// </param>
 330    /// <returns><see langword="true"/> if the specified <see cref="ServiceAddress"/> is equal to the current
 331    /// <see cref="ServiceAddress"/>; otherwise, <see langword="false"/>.</returns>
 332    public bool Equals(ServiceAddress? other)
 256333    {
 256334        if (other is null)
 2335        {
 2336            return false;
 337        }
 254338        else if (ReferenceEquals(this, other))
 0339        {
 0340            return true;
 341        }
 342
 254343        if (Protocol != other.Protocol)
 2344        {
 2345            return false;
 346        }
 347
 252348        if (Protocol is null)
 14349        {
 350            // Both service addresses are relative
 14351            return Path == other.Path;
 352        }
 353
 354        // Comparing 2 service addresses with the same protocol
 238355        return Path == other.Path &&
 238356            Fragment == other.Fragment &&
 238357            ServerAddress == other.ServerAddress &&
 238358            AltServerAddresses.SequenceEqual(other.AltServerAddresses) &&
 238359            Params.DictionaryEqual(other.Params);
 256360    }
 361
 362    /// <summary>Serves as the default hash function.</summary>
 363    /// <returns>A hash code for the current <see cref="ServiceAddress"/>.</returns>
 364    public override int GetHashCode()
 282365    {
 282366        if (Protocol is null)
 18367        {
 18368            return Path.GetHashCode(StringComparison.Ordinal);
 369        }
 370
 371        // We only hash a subset of the properties to keep GetHashCode reasonably fast.
 264372        var hash = new HashCode();
 264373        hash.Add(Protocol);
 264374        hash.Add(Path);
 264375        hash.Add(Fragment);
 264376        hash.Add(_serverAddress);
 264377        hash.Add(_altServerAddresses.Count);
 264378        return hash.ToHashCode();
 282379    }
 380
 381    /// <summary>Converts this service address into a string.</summary>
 382    /// <returns>The string representation of this service address.</returns>
 383    public override string ToString()
 450384    {
 450385        if (Protocol is null)
 33386        {
 33387            return Path;
 388        }
 417389        else if (OriginalUri is Uri uri)
 389390        {
 389391            return uri.ToString();
 392        }
 393
 394        // else, construct a string with a string builder.
 395
 28396        var sb = new StringBuilder();
 28397        bool firstOption = true;
 398
 28399        if (ServerAddress is ServerAddress serverAddress)
 4400        {
 4401            sb.AppendServerAddress(serverAddress, Path);
 4402            firstOption = serverAddress.Params.Count == 0;
 4403        }
 404        else
 24405        {
 24406            sb.Append(Protocol);
 24407            sb.Append(':');
 24408            sb.Append(Path);
 24409        }
 410
 28411        if (AltServerAddresses.Count > 0)
 4412        {
 4413            StartQueryOption(sb, ref firstOption);
 4414            sb.Append("alt-server=");
 24415            for (int i = 0; i < AltServerAddresses.Count; ++i)
 8416            {
 8417                if (i > 0)
 4418                {
 4419                    sb.Append(',');
 4420                }
 8421                sb.AppendServerAddress(AltServerAddresses[i], path: "", includeScheme: false, paramSeparator: '$');
 8422            }
 4423        }
 424
 92425        foreach ((string name, string value) in Params)
 4426        {
 4427            StartQueryOption(sb, ref firstOption);
 4428            sb.Append(name);
 4429            if (value.Length > 0)
 4430            {
 4431                sb.Append('=');
 4432                sb.Append(value);
 4433            }
 4434        }
 435
 28436        if (Fragment.Length > 0)
 4437        {
 4438            sb.Append('#');
 4439            sb.Append(Fragment);
 4440        }
 441
 28442        return sb.ToString();
 443
 444        static void StartQueryOption(StringBuilder sb, ref bool firstOption)
 8445        {
 8446            if (firstOption)
 4447            {
 4448                sb.Append('?');
 4449                firstOption = false;
 4450            }
 451            else
 4452            {
 4453                sb.Append('&');
 4454            }
 8455        }
 450456    }
 457
 458    /// <summary>Converts this service address into a Uri.</summary>
 459    /// <returns>An Uri representing this service address.</returns>
 460    public Uri ToUri() =>
 6461        OriginalUri ?? (Protocol is null ? new Uri(Path, UriKind.Relative) : new Uri(ToString(), UriKind.Absolute));
 462
 463    /// <summary>Checks if <paramref name="params" /> contains properly escaped names and values.</summary>
 464    /// <param name="params">The dictionary to check.</param>
 465    /// <exception cref="FormatException">Thrown if the dictionary is not valid.</exception>
 466    /// <remarks>A dictionary returned by <see cref="UriExtensions.ParseQuery" /> is properly escaped.</remarks>
 467    internal static void CheckParams(ImmutableDictionary<string, string> @params)
 125468    {
 499469        foreach ((string name, string value) in @params)
 67470        {
 67471            if (!IsValidParamName(name))
 6472            {
 6473                throw new FormatException($"Invalid parameter name '{name}'.");
 474            }
 61475            if (!IsValidParamValue(value))
 4476            {
 4477                throw new FormatException($"Invalid parameter value '{value}'.");
 478            }
 57479        }
 115480    }
 481
 482    /// <summary>Checks if <paramref name="path" /> is a properly escaped URI absolute path, i.e. that it starts
 483    /// with a <c>/</c> and contains only unreserved characters, <c>%</c>, and reserved characters other than
 484    /// <c>?</c> and <c>#</c>.</summary>
 485    /// <param name="path">The path to check.</param>
 486    /// <exception cref="FormatException">Thrown if the path is not valid.</exception>
 487    /// <remarks>The absolute path of a URI with a supported protocol satisfies these requirements.</remarks>
 488    internal static void CheckPath(string path)
 244489    {
 244490        if (path.Length == 0 || path[0] != '/' || !IsValid(path, _notValidInPath))
 8491        {
 8492            throw new FormatException(
 8493                $"Invalid path '{path}'; a valid path starts with '/' and contains only unreserved characters, '%', and 
 494        }
 236495    }
 496
 497    /// <summary>Checks if <paramref name="value" /> contains only unreserved characters, <c>%</c>, and reserved
 498    /// characters other than <c>#</c> and <c>&#38;</c>.</summary>
 499    /// <param name="value">The value to check.</param>
 500    /// <returns><see langword="true" /> if <paramref name="value" /> is a valid parameter value; otherwise,
 501    /// <see langword="false" />.</returns>
 2713502    internal static bool IsValidParamValue(string value) => IsValid(value, _notValidInParamValue);
 503
 504    /// <summary>"unchecked" constructor used by the Slice decoder when decoding a Slice1 encoded service address.
 505    /// </summary>
 37506    internal ServiceAddress(
 37507        Protocol protocol,
 37508        string path,
 37509        ServerAddress? serverAddress,
 37510        ImmutableList<ServerAddress> altServerAddresses,
 37511        ImmutableDictionary<string, string> serviceAddressParams,
 37512        string fragment)
 37513    {
 37514        Protocol = protocol;
 37515        _path = path;
 37516        _serverAddress = serverAddress;
 37517        _altServerAddresses = altServerAddresses;
 37518        _params = serviceAddressParams;
 37519        _fragment = fragment;
 37520    }
 521
 522    /// <summary>Checks if <paramref name="fragment" /> is a properly escaped URI fragment, i.e. it contains only
 523    /// unreserved characters, reserved characters, and '%'.</summary>
 524    /// <param name="fragment">The fragment to check.</param>
 525    /// <exception cref="FormatException">Thrown if the fragment is not valid.</exception>
 526    /// <remarks>The fragment of a URI with a supported protocol satisfies these requirements.</remarks>
 527    private static void CheckFragment(string fragment)
 6528    {
 6529        if (!IsValid(fragment, _notValidInFragment))
 2530        {
 2531            throw new FormatException(
 2532                $"Invalid fragment '{fragment}'; a valid fragment contains only unreserved characters, reserved characte
 533        }
 4534    }
 535
 536    private static bool IsValid(string s, SearchValues<char> invalidChars)
 3020537    {
 3020538        ReadOnlySpan<char> span = s.AsSpan();
 3020539        return span.IndexOfAnyExceptInRange(FirstValidChar, LastValidChar) == -1 && span.IndexOfAny(invalidChars) == -1;
 3020540    }
 541
 542    /// <summary>Checks if <paramref name="name" /> is not empty, not equal to <c>alt-server</c> nor equal to
 543    /// <c>transport</c> and contains only unreserved characters, <c>%</c>, or reserved characters other than <c>#</c>,
 544    /// <c>&#38;</c> and <c>=</c>.</summary>
 545    /// <param name="name">The name to check.</param>
 546    /// <returns><see langword="true" /> if <paramref name="name" /> is a valid parameter name; otherwise,
 547    /// <see langword="false" />.</returns>
 548    /// <remarks>The range of valid names is much larger than the range of names you should use. For example, you
 549    /// should avoid parameter names with a <c>%</c> or <c>$</c> character, even though these characters are valid
 550    /// in a name.</remarks>
 551    private static bool IsValidParamName(string name) =>
 67552        name.Length > 0 && name != "alt-server" && name != "transport" && IsValid(name, _notValidInParamName);
 553}
 554
 555/// <summary>The service address type converter specifies how to convert a string to a service address. It's used by
 556/// sub-systems such as the Microsoft ConfigurationBinder to bind string values to ServiceAddress properties.</summary>
 557public class ServiceAddressTypeConverter : TypeConverter
 558{
 559    /// <summary>Returns whether this converter can convert an object of the given type into a
 560    /// <see cref="ServiceAddress"/> object, using the specified context.</summary>
 561    /// <param name="context">An <see cref="ITypeDescriptorContext"/> that provides a format context.</param>
 562    /// <param name="sourceType">A <see cref="Type"/> that represents the type you want to convert from.</param>
 563    /// <returns><see langword="true"/>if this converter can perform the conversion; otherwise, <see langword="false"/>.
 564    /// </returns>
 565    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) =>
 566        sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
 567
 568    /// <summary>Converts the given object into a <see cref="ServiceAddress"/> object, using the specified context and c
 569    /// information.</summary>
 570    /// <param name="context">An <see cref="ITypeDescriptorContext"/> that provides a format context.</param>
 571    /// <param name="culture">The <see cref="CultureInfo"/> to use as the current culture.</param>
 572    /// <param name="value">The <see cref="object "/> to convert.</param>
 573    /// <returns>An <see cref="object "/> that represents the converted <see cref="ServiceAddress"/>.</returns>
 574    /// <remarks><see cref="TypeConverter"/>.</remarks>
 575    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) =>
 576        value is string valueStr ? new ServiceAddress(new Uri(valueStr)) : base.ConvertFrom(context, culture, value);
 577}