< Summary

Information
Class: IceRpc.ServiceAddress
Assembly: IceRpc
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/ServiceAddress.cs
Tag: 701_22528036593
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>
 591122    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    {
 61529        get => _serverAddress;
 30
 31        init
 2232        {
 2233            if (Protocol is null)
 134            {
 135                throw new InvalidOperationException(
 136                    $"Cannot set {nameof(ServerAddress)} on a relative service address.");
 37            }
 38
 2139            if (value?.Protocol is Protocol newProtocol && newProtocol != Protocol)
 140            {
 141                throw new ArgumentException(
 142                    $"The {nameof(ServerAddress)} must use the service address's protocol: '{Protocol}'.",
 143                    nameof(value));
 44            }
 45
 2046            if (value is not null)
 1947            {
 1948                if (_params.Count > 0)
 149                {
 150                    throw new InvalidOperationException(
 151                        $"Cannot set {nameof(ServerAddress)} on a service address with parameters.");
 52                }
 1853            }
 154            else if (_altServerAddresses.Count > 0)
 155            {
 156                throw new InvalidOperationException(
 157                    $"Cannot clear {nameof(ServerAddress)} when {nameof(AltServerAddresses)} is not empty.");
 58            }
 1859            _serverAddress = value;
 1860            OriginalUri = null;
 1861        }
 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    {
 346568        get => _path;
 69        init
 6770        {
 71            try
 6772            {
 6773                CheckPath(value); // make sure it's properly escaped
 6674                Protocol?.CheckPath(value); // make sure the protocol is happy with this path
 6575            }
 276            catch (FormatException exception)
 277            {
 278                throw new ArgumentException("Invalid path.", nameof(value), exception);
 79            }
 6580            _path = value;
 6581            OriginalUri = null;
 6582        }
 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    {
 52090        get => _altServerAddresses;
 91
 92        init
 793        {
 794            if (Protocol is null)
 095            {
 096                throw new InvalidOperationException(
 097                    $"Cannot set {nameof(AltServerAddresses)} on a relative service address.");
 98            }
 99
 7100            if (value.Count > 0)
 7101            {
 7102                if (_serverAddress is null)
 1103                {
 1104                    throw new InvalidOperationException(
 1105                        $"Cannot set {nameof(AltServerAddresses)} when {nameof(ServerAddress)} is empty.");
 106                }
 107
 16108                if (value.Any(e => e.Protocol != Protocol))
 1109                {
 1110                    throw new ArgumentException(
 1111                        $"The {nameof(AltServerAddresses)} server addresses must use the service address's protocol: '{P
 1112                        nameof(value));
 113                }
 5114            }
 115            // else, no need to check anything, an empty list is always fine.
 116
 5117            _altServerAddresses = value;
 5118            OriginalUri = null;
 5119        }
 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    {
 324127        get => _params;
 128        init
 65129        {
 65130            if (Protocol is null)
 0131            {
 0132                throw new InvalidOperationException($"Cannot set {nameof(Params)} on a relative service address.");
 133            }
 134
 135            try
 65136            {
 65137                CheckParams(value); // general checking (properly escape, no empty name)
 65138                Protocol.CheckServiceAddressParams(value); // protocol-specific checking
 64139            }
 1140            catch (FormatException exception)
 1141            {
 1142                throw new ArgumentException("Invalid parameters.", nameof(value), exception);
 143            }
 144
 64145            if (_serverAddress is not null && value.Count > 0)
 1146            {
 1147                throw new InvalidOperationException(
 1148                    $"Cannot set {nameof(Params)} on a service address with a serverAddress.");
 149            }
 150
 63151            _params = value;
 63152            OriginalUri = null;
 63153        }
 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    {
 3343160        get => _fragment;
 161        init
 4162        {
 4163            if (Protocol is null)
 1164            {
 1165                throw new InvalidOperationException($"Cannot set {nameof(Fragment)} on a relative service address.");
 166            }
 167
 168            try
 3169            {
 3170                CheckFragment(value); // make sure it's properly escaped
 2171            }
 1172            catch (FormatException exception)
 1173            {
 1174                throw new ArgumentException("Invalid fragment.", nameof(value), exception);
 175            }
 176
 2177            if (!Protocol.HasFragment && value.Length > 0)
 1178            {
 1179                throw new InvalidOperationException($"Cannot set {Fragment} on an {Protocol} service address.");
 180            }
 181
 1182            _fragment = value;
 1183            OriginalUri = null;
 1184        }
 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>
 812191    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
 12198    private static readonly SearchValues<char> _notValidInFragment = SearchValues.Create("\"<>\\^`{|}");
 12199    private static readonly SearchValues<char> _notValidInParamName = SearchValues.Create("\"<>#&=\\^`{|}");
 12200    private static readonly SearchValues<char> _notValidInParamValue = SearchValues.Create("\"<>#&\\^`{|}");
 12201    private static readonly SearchValues<char> _notValidInPath = SearchValues.Create("\"<>#?\\^`{|}");
 202
 3348203    private ImmutableList<ServerAddress> _altServerAddresses = ImmutableList<ServerAddress>.Empty;
 3348204    private string _fragment = "";
 3348205    private ImmutableDictionary<string, string> _params = ImmutableDictionary<string, string>.Empty;
 3348206    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>
 5728211    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>
 447215    public ServiceAddress(Uri uri)
 447216    {
 447217        if (uri.IsAbsoluteUri)
 430218        {
 430219            Protocol = Protocol.TryParse(uri.Scheme, out Protocol? protocol) ? protocol :
 430220                throw new ArgumentException(
 430221                    $"Cannot create a service address with protocol '{uri.Scheme}'.",
 430222                    nameof(uri));
 223
 224            // The AbsolutePath is empty for a URI such as "icerpc:?foo=bar"
 429225            _path = uri.AbsolutePath.Length > 0 ? uri.AbsolutePath : "/";
 429226            _fragment = uri.Fragment.Length > 0 ? uri.Fragment[1..] : ""; // remove leading #
 227
 228            try
 429229            {
 429230                Protocol.CheckPath(_path);
 428231            }
 1232            catch (FormatException exception)
 1233            {
 1234                throw new ArgumentException($"Invalid path in {Protocol} URI.", nameof(uri), exception);
 235            }
 236
 428237            if (!Protocol.HasFragment && _fragment.Length > 0)
 2238            {
 2239                throw new ArgumentException(
 2240                    $"Cannot create an {Protocol} service address with a fragment.",
 2241                    nameof(uri));
 242            }
 243
 426244            (ImmutableDictionary<string, string> queryParams, string? altServerValue, string? transport) =
 426245                uri.ParseQuery();
 246
 426247            if (uri.Authority.Length > 0)
 362248            {
 362249                if (uri.UserInfo.Length > 0)
 1250                {
 1251                    throw new ArgumentException("Cannot create a server address with a user info.", nameof(uri));
 252                }
 253
 361254                string host = uri.IdnHost;
 361255                Debug.Assert(host.Length > 0); // the IdnHost provided by Uri is never empty
 256
 361257                _serverAddress = new ServerAddress(
 361258                    Protocol,
 361259                    host,
 361260                    port: uri.Port == -1 ? Protocol.DefaultPort : checked((ushort)uri.Port),
 361261                    transport,
 361262                    queryParams);
 263
 361264                if (altServerValue is not null)
 43265                {
 266                    // Split and parse recursively each serverAddress
 292267                    foreach (string serverAddressStr in altServerValue.Split(','))
 83268                    {
 83269                        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.
 83274                        _altServerAddresses = _altServerAddresses.Add(
 83275                            new ServerAddress(new Uri(altUriString.Replace('$', '&'))));
 80276                    }
 40277                }
 358278            }
 279            else
 64280            {
 64281                if (!_path.StartsWith('/'))
 1282                {
 1283                    throw new ArgumentException(
 1284                        $"Invalid path in service address URI '{uri.OriginalString}'.",
 1285                        nameof(uri));
 286                }
 287
 63288                if (altServerValue is not null)
 1289                {
 1290                    throw new ArgumentException(
 1291                        $"Invalid alt-server parameter in URI '{uri.OriginalString}'.",
 1292                        nameof(uri));
 293                }
 294
 295                try
 62296                {
 62297                    Protocol.CheckServiceAddressParams(queryParams);
 60298                }
 2299                catch (FormatException exception)
 2300                {
 2301                    throw new ArgumentException("Invalid parameters in URI.", nameof(uri), exception);
 302                }
 303
 60304                Params = queryParams;
 60305            }
 418306        }
 307        else
 17308        {
 309            // relative service address
 17310            Protocol = null;
 17311            _path = uri.ToString();
 312
 313            try
 17314            {
 17315                CheckPath(_path);
 17316            }
 0317            catch (FormatException exception)
 0318            {
 0319                throw new ArgumentException("Invalid path in relative URI.", nameof(uri), exception);
 320            }
 17321        }
 322
 435323        OriginalUri = uri;
 435324    }
 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)
 151333    {
 151334        if (other is null)
 1335        {
 1336            return false;
 337        }
 150338        else if (ReferenceEquals(this, other))
 0339        {
 0340            return true;
 341        }
 342
 150343        if (Protocol != other.Protocol)
 1344        {
 1345            return false;
 346        }
 347
 149348        if (Protocol is null)
 7349        {
 350            // Both service addresses are relative
 7351            return Path == other.Path;
 352        }
 353
 354        // Comparing 2 service addresses with the same protocol
 142355        return Path == other.Path &&
 142356            Fragment == other.Fragment &&
 142357            ServerAddress == other.ServerAddress &&
 142358            AltServerAddresses.SequenceEqual(other.AltServerAddresses) &&
 142359            Params.DictionaryEqual(other.Params);
 151360    }
 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()
 141365    {
 141366        if (Protocol is null)
 9367        {
 9368            return Path.GetHashCode(StringComparison.Ordinal);
 369        }
 370
 371        // We only hash a subset of the properties to keep GetHashCode reasonably fast.
 132372        var hash = new HashCode();
 132373        hash.Add(Protocol);
 132374        hash.Add(Path);
 132375        hash.Add(Fragment);
 132376        hash.Add(_serverAddress);
 132377        hash.Add(_altServerAddresses.Count);
 132378        return hash.ToHashCode();
 141379    }
 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()
 240384    {
 240385        if (Protocol is null)
 18386        {
 18387            return Path;
 388        }
 222389        else if (OriginalUri is Uri uri)
 205390        {
 205391            return uri.ToString();
 392        }
 393
 394        // else, construct a string with a string builder.
 395
 17396        var sb = new StringBuilder();
 17397        bool firstOption = true;
 398
 17399        if (ServerAddress is ServerAddress serverAddress)
 2400        {
 2401            sb.AppendServerAddress(serverAddress, Path);
 2402            firstOption = serverAddress.Params.Count == 0;
 2403        }
 404        else
 15405        {
 15406            sb.Append(Protocol);
 15407            sb.Append(':');
 15408            sb.Append(Path);
 15409        }
 410
 17411        if (AltServerAddresses.Count > 0)
 2412        {
 2413            StartQueryOption(sb, ref firstOption);
 2414            sb.Append("alt-server=");
 12415            for (int i = 0; i < AltServerAddresses.Count; ++i)
 4416            {
 4417                if (i > 0)
 2418                {
 2419                    sb.Append(',');
 2420                }
 4421                sb.AppendServerAddress(AltServerAddresses[i], path: "", includeScheme: false, paramSeparator: '$');
 4422            }
 2423        }
 424
 55425        foreach ((string name, string value) in Params)
 2426        {
 2427            StartQueryOption(sb, ref firstOption);
 2428            sb.Append(name);
 2429            if (value.Length > 0)
 2430            {
 2431                sb.Append('=');
 2432                sb.Append(value);
 2433            }
 2434        }
 435
 17436        if (Fragment.Length > 0)
 2437        {
 2438            sb.Append('#');
 2439            sb.Append(Fragment);
 2440        }
 441
 17442        return sb.ToString();
 443
 444        static void StartQueryOption(StringBuilder sb, ref bool firstOption)
 4445        {
 4446            if (firstOption)
 2447            {
 2448                sb.Append('?');
 2449                firstOption = false;
 2450            }
 451            else
 2452            {
 2453                sb.Append('&');
 2454            }
 4455        }
 240456    }
 457
 458    /// <summary>Converts this service address into a Uri.</summary>
 459    /// <returns>An Uri representing this service address.</returns>
 460    public Uri ToUri() =>
 3461        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)
 77468    {
 302469        foreach ((string name, string value) in @params)
 38470        {
 38471            if (!IsValidParamName(name))
 3472            {
 3473                throw new FormatException($"Invalid parameter name '{name}'.");
 474            }
 35475            if (!IsValidParamValue(value))
 2476            {
 2477                throw new FormatException($"Invalid parameter value '{value}'.");
 478            }
 33479        }
 72480    }
 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)
 155489    {
 155490        if (path.Length == 0 || path[0] != '/' || !IsValid(path, _notValidInPath))
 4491        {
 4492            throw new FormatException(
 4493                $"Invalid path '{path}'; a valid path starts with '/' and contains only unreserved characters, '%', and 
 494        }
 151495    }
 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>
 1399502    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)
 3528    {
 3529        if (!IsValid(fragment, _notValidInFragment))
 1530        {
 1531            throw new FormatException(
 1532                $"Invalid fragment '{fragment}'; a valid fragment contains only unreserved characters, reserved characte
 533        }
 2534    }
 535
 536    private static bool IsValid(string s, SearchValues<char> invalidChars)
 1590537    {
 1590538        ReadOnlySpan<char> span = s.AsSpan();
 1590539        return span.IndexOfAnyExceptInRange(FirstValidChar, LastValidChar) == -1 && span.IndexOfAny(invalidChars) == -1;
 1590540    }
 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) =>
 38552        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}