├── src ├── Uris.Tests │ ├── stryke.bat │ ├── stryker-config.json │ ├── Urls.Tests.csproj │ └── UriTests.cs ├── Uris.TestsFSharp │ ├── Program.fs │ ├── Urls.TestsFSharp.fsproj │ └── Tests.fs ├── Uris │ ├── IsExternalInit.cs │ ├── Urls.csproj │ ├── UserInfo.cs │ ├── QueryParameter.cs │ ├── Urls.xml │ ├── AbsoluteUrl.cs │ ├── RelativeUrl.cs │ └── UrlExtensions.cs ├── Directory.Build.props ├── Urls.sln └── .editorconfig ├── Images ├── CodeCoverage.png └── MutationScore.png ├── .config └── dotnet-tools.json ├── .gitignore ├── .github └── workflows │ └── dotnet.yml ├── LICENSE └── README.md /src/Uris.Tests/stryke.bat: -------------------------------------------------------------------------------- 1 | dotnet stryker -------------------------------------------------------------------------------- /src/Uris.TestsFSharp/Program.fs: -------------------------------------------------------------------------------- 1 | module Program = let [] main _ = 0 2 | -------------------------------------------------------------------------------- /Images/CodeCoverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MelbourneDeveloper/Urls/HEAD/Images/CodeCoverage.png -------------------------------------------------------------------------------- /Images/MutationScore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MelbourneDeveloper/Urls/HEAD/Images/MutationScore.png -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "coveralls.net": { 6 | "version": "3.0.0", 7 | "commands": [ 8 | "csmacnz.Coveralls" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Uris.Tests/stryker-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/stryker-mutator/stryker-net/master/src/Stryker.CLI/Stryker.CLI/schema.json", 3 | "stryker-config": { 4 | "ignore-methods": [ 5 | "*ConfigureAwait" 6 | ] 7 | } 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.Designer.cs 2 | *.csproj.user 3 | /NuGet/Exported/ 4 | bin/ 5 | obj/ 6 | packages/ 7 | /.vs/restore.dg 8 | *.suo 9 | .vs 10 | *.pdb 11 | *.dll 12 | project.fragment.lock.json 13 | project.lock.json 14 | *.CodeAnalysisLog.xml 15 | *.lastcodeanalysissucceeded 16 | 17 | src/.idea/* 18 | 19 | *.user 20 | 21 | 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /src/Uris/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0130 // Namespace does not match folder structure 2 | #pragma warning disable CA1515 // Consider making public types internal 3 | 4 | namespace System.Runtime.CompilerServices; 5 | 6 | public class IsExternalInit { } 7 | 8 | #pragma warning restore IDE0130 // Namespace does not match folder structure 9 | #pragma warning restore CA1515 // Consider making public types internal 10 | -------------------------------------------------------------------------------- /src/Uris/Urls.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | netstandard2.1;net9.0 6 | true 7 | Url Uri 8 | 9 | 10 | 11 | Urls.xml 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Uris.TestsFSharp/Urls.TestsFSharp.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Uris/UserInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Urls; 2 | 3 | /// 4 | /// Url credentials. Warning: using this is not recommended. This is here for completeness 5 | /// 6 | public record UserInfo(string Username, string Password) 7 | { 8 | #region Public Properties 9 | public static UserInfo Empty { get; } = new("", ""); 10 | #endregion 11 | 12 | #region Constructors 13 | public UserInfo(UserInfo userInfo) 14 | { 15 | userInfo ??= Empty; 16 | 17 | Username = userInfo.Username; 18 | Password = userInfo.Password; 19 | } 20 | #endregion 21 | 22 | #region Public Methods 23 | public override string ToString() => 24 | $"{(!string.IsNullOrEmpty(Username) ? $"{Username}:{Password}@" : "")}"; 25 | #endregion 26 | } 27 | -------------------------------------------------------------------------------- /src/Uris.Tests/Urls.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | Latest 6 | false 7 | false 8 | false 9 | false 10 | false 11 | $(NoWarn);CA1515;CA1861 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Setup .NET 5 18 | uses: actions/setup-dotnet@v1 19 | with: 20 | dotnet-version: 5.0.x 21 | 22 | - name: Setup .NET Core 3.1 23 | uses: actions/setup-dotnet@v1 24 | with: 25 | dotnet-version: 3.1.x 26 | 27 | - name: Restore dependencies 28 | run: dotnet restore src/Urls.sln 29 | 30 | - name: Build 31 | run: dotnet build src/Urls.sln --no-restore 32 | 33 | - name: Test 34 | run: dotnet test src/Urls.sln --no-build --verbosity normal 35 | 36 | # - name: Coveralls 37 | # uses: coverallsapp/github-action@master 38 | # with: 39 | # github-token: ${{ secrets.GITHUB_TOKEN }} 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Christian Findlay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Uris.TestsFSharp/Tests.fs: -------------------------------------------------------------------------------- 1 | namespace Uris.TestsFSharp 2 | 3 | open System 4 | open Microsoft.VisualStudio.TestTools.UnitTesting 5 | open Urls 6 | 7 | [] 8 | type TestClass () = 9 | 10 | 11 | 12 | [] 13 | member this.TestStringToAbsoluteUri () = 14 | 15 | let uri = "http://username:password@host.com:5000/pathpart1/pathpart2?fieldname1=field%3C%3EValue1&FieldName2=field%3C%3EValue2#frag".ToAbsoluteUrl() 16 | 17 | Assert.AreEqual("frag", uri.RelativeUrl.Fragment) 18 | Assert.AreEqual("username:password@", uri.UserInfo.ToString()) 19 | 20 | 21 | [] 22 | member this.TestComposition () = 23 | 24 | let uri = 25 | "host.com".ToHttpUrlFromHost(5000) 26 | .AddQueryParameter("fieldname1", "field<>Value1") 27 | .WithCredentials("username", "password") 28 | .AddQueryParameter("FieldName2", "field<>Value2") 29 | .WithFragment("frag") 30 | .WithPath("pathpart1", "pathpart2") 31 | 32 | Assert.AreEqual("http://username:password@host.com:5000/pathpart1/pathpart2?fieldname1=field%3C%3EValue1&FieldName2=field%3C%3EValue2#frag",uri.ToString()); 33 | 34 | -------------------------------------------------------------------------------- /src/Uris/QueryParameter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Net; 4 | 5 | namespace Urls; 6 | 7 | /// 8 | /// Represents a singular Query parameter as part of a Query String 9 | /// 10 | public record QueryParameter 11 | { 12 | #region Fields 13 | private string? fieldValue; 14 | #endregion 15 | 16 | #region Public Properties 17 | public static ImmutableList EmptyList { get; } = 18 | ImmutableList.Empty; 19 | 20 | public string FieldName { get; init; } 21 | public string? Value 22 | { 23 | get => fieldValue; 24 | init { fieldValue = WebUtility.UrlDecode(value); } 25 | } 26 | #endregion 27 | 28 | #region Constructors 29 | public QueryParameter(string fieldName, string? value) 30 | { 31 | FieldName = fieldName ?? throw new ArgumentNullException(nameof(fieldName)); 32 | fieldValue = WebUtility.UrlDecode(value); 33 | } 34 | #endregion 35 | 36 | #region Public Methods 37 | public override string ToString() => 38 | $"{FieldName}{(Value != null ? "=" : "")}{WebUtility.UrlEncode(Value)}"; 39 | #endregion 40 | } 41 | -------------------------------------------------------------------------------- /src/Uris/Urls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Urls 5 | 6 | 7 | 8 | 9 | Represents a Url with all information 10 | 11 | 12 | 13 | 14 | Represents a Url with all information 15 | 16 | 17 | 18 | 19 | Represents a singular Query parameter as part of a Query String 20 | 21 | 22 | 23 | 24 | Represents a Url without the specifics of the server address 25 | 26 | 27 | 28 | 29 | Represents a Url without the specifics of the server address 30 | 31 | 32 | 33 | 34 | Url credentials. Warning: using this is not recommended. This is here for completeness 35 | 36 | 37 | 38 | 39 | Url credentials. Warning: using this is not recommended. This is here for completeness 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Uris/AbsoluteUrl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #pragma warning disable CA2225 // Operator overloads have named alternates 4 | 5 | namespace Urls; 6 | 7 | /// 8 | /// Represents a Url with all information 9 | /// 10 | public record AbsoluteUrl( 11 | string Scheme, 12 | string Host, 13 | int? Port, 14 | RelativeUrl RelativeUrl, 15 | UserInfo UserInfo 16 | ) 17 | { 18 | #region Public Properties 19 | public static AbsoluteUrl Empty { get; } = new AbsoluteUrl("", ""); 20 | #endregion 21 | 22 | #region Constructors 23 | public AbsoluteUrl( 24 | string scheme, 25 | string host, 26 | int? port = null, 27 | RelativeUrl? requestUri = null 28 | ) 29 | : this(scheme, host, port, requestUri ?? RelativeUrl.Empty, UserInfo.Empty) { } 30 | 31 | public AbsoluteUrl(AbsoluteUrl absoluteUrl) 32 | { 33 | absoluteUrl ??= Empty; 34 | 35 | Scheme = absoluteUrl.Scheme; 36 | Host = absoluteUrl.Host; 37 | Port = absoluteUrl.Port; 38 | RelativeUrl = new RelativeUrl(absoluteUrl.RelativeUrl); 39 | UserInfo = absoluteUrl.UserInfo; 40 | } 41 | 42 | //TODO: Parse the string instead of converting to a Uri first 43 | #pragma warning disable CA1054 // URI-like parameters should not be strings 44 | public AbsoluteUrl(string uriString) 45 | : this(new Uri(uriString).ToAbsoluteUrl()) 46 | #pragma warning restore CA1054 // URI-like parameters should not be strings 47 | { } 48 | #endregion 49 | 50 | #region Public Methods 51 | public override string ToString() => 52 | $"{Scheme}://" 53 | + UserInfo 54 | + $"{Host}" 55 | + (Port.HasValue ? $":{Port.Value}" : "") 56 | + RelativeUrl; 57 | #endregion 58 | 59 | #region Operators 60 | public static implicit operator Uri(AbsoluteUrl absoluteUrl) => 61 | absoluteUrl == null ? Empty : new Uri(absoluteUrl.ToString()); 62 | 63 | public static explicit operator AbsoluteUrl(Uri uri) => uri.ToAbsoluteUrl(); 64 | #endregion 65 | } 66 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | true 6 | true 7 | true 8 | true 9 | latest 10 | AllEnabledByDefault 11 | true 12 | true 13 | true 14 | true 15 | enable 16 | Latest 17 | 1.0.0 18 | true 19 | true 20 | Christian Findlay 21 | Christian Findlay 22 | © $([System.DateTime]::UtcNow.ToString(yyyy)) Christian Findlay 23 | git 24 | MIT 25 | Uri Url 26 | true 27 | https://github.com/MelbourneDeveloper/Urls 28 | $(NoWarn);IDE0301;IDE0303;IDE0305;CA1515 29 | $(WarningsNotAsErrors);IDE0301;IDE0303;IDE0305 30 | 31 | 32 | 33 | all 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | 36 | 37 | all 38 | runtime; build; native; contentfiles; analyzers; buildtransitive 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/Urls.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31112.23 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Urls", "Uris\Urls.csproj", "{225707BA-9780-4E54-83F0-E9A97715472C}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3260000C-FEA0-48DC-87DF-3AE3293D7394}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | Directory.Build.props = Directory.Build.props 12 | ..\.github\workflows\dotnet.yml = ..\.github\workflows\dotnet.yml 13 | EndProjectSection 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Urls.Tests", "Uris.Tests\Urls.Tests.csproj", "{68BF6DAF-015F-4B6E-8C7A-EADDC7476BF6}" 16 | EndProject 17 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Urls.TestsFSharp", "Uris.TestsFSharp\Urls.TestsFSharp.fsproj", "{C16A5161-F7BE-4676-BF66-0D33AD6A071F}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {225707BA-9780-4E54-83F0-E9A97715472C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {225707BA-9780-4E54-83F0-E9A97715472C}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {225707BA-9780-4E54-83F0-E9A97715472C}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {225707BA-9780-4E54-83F0-E9A97715472C}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {68BF6DAF-015F-4B6E-8C7A-EADDC7476BF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {68BF6DAF-015F-4B6E-8C7A-EADDC7476BF6}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {68BF6DAF-015F-4B6E-8C7A-EADDC7476BF6}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {68BF6DAF-015F-4B6E-8C7A-EADDC7476BF6}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {C16A5161-F7BE-4676-BF66-0D33AD6A071F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {C16A5161-F7BE-4676-BF66-0D33AD6A071F}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {C16A5161-F7BE-4676-BF66-0D33AD6A071F}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {C16A5161-F7BE-4676-BF66-0D33AD6A071F}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | GlobalSection(ExtensibilityGlobals) = postSolution 42 | SolutionGuid = {BEE2A3E2-B291-44F9-904E-A9639C2EB6FA} 43 | EndGlobalSection 44 | EndGlobal 45 | -------------------------------------------------------------------------------- /src/Uris/RelativeUrl.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | #if !NET45 5 | using System; 6 | #endif 7 | 8 | namespace Urls; 9 | 10 | /// 11 | /// Represents a Url without the specifics of the server address 12 | /// 13 | public record RelativeUrl( 14 | ImmutableList Path, 15 | ImmutableList QueryParameters 16 | ) 17 | { 18 | #region Fields 19 | private string fragment = ""; 20 | #endregion 21 | 22 | #region Public Properties 23 | public string Fragment 24 | { 25 | get => fragment; 26 | init { fragment = value ?? ""; } 27 | } 28 | #endregion 29 | 30 | #region Constructors 31 | public RelativeUrl( 32 | IReadOnlyList? path = null, 33 | IReadOnlyList? query = null, 34 | string fragment = "" 35 | ) 36 | : this( 37 | path?.ToImmutableList() ?? ImmutableList.Empty, 38 | query?.ToImmutableList() ?? QueryParameter.EmptyList 39 | ) => this.fragment = fragment ?? ""; 40 | 41 | public RelativeUrl( 42 | #pragma warning disable CA1054 // URI-like parameters should not be strings 43 | string relativeUrlString) 44 | : this(relativeUrlString.ToRelativeUrl()) 45 | #pragma warning restore CA1054 // URI-like parameters should not be strings 46 | { } 47 | 48 | public RelativeUrl(RelativeUrl relativeUrl) 49 | { 50 | relativeUrl ??= Empty; 51 | 52 | Path = relativeUrl.Path; 53 | QueryParameters = relativeUrl.QueryParameters; 54 | fragment = relativeUrl.Fragment ?? ""; 55 | } 56 | 57 | public static RelativeUrl Empty { get; } = 58 | new(ImmutableList.Empty, QueryParameter.EmptyList); 59 | #endregion 60 | 61 | #region Public Methods 62 | public override string ToString() => 63 | (Path.Count > 0 ? $"/{string.Join("/", Path)}" : "") 64 | + ( 65 | QueryParameters.Count > 0 66 | ? $"?{string.Join("&", QueryParameters.Select(e => e.ToString()))}" 67 | : "" 68 | ) 69 | + (!string.IsNullOrEmpty(Fragment) ? $"#{Fragment}" : ""); 70 | 71 | public virtual bool Equals(RelativeUrl? other) => 72 | other != null 73 | && string.CompareOrdinal(other.Fragment, Fragment) == 0 74 | && other.Path.SequenceEqual(Path) 75 | && other.QueryParameters.SequenceEqual(QueryParameters); 76 | 77 | // Optional: warning generated if not supplied when Equals(R?) is user-defined. 78 | public override int GetHashCode() => 79 | ToString().GetHashCode(StringComparison.InvariantCultureIgnoreCase); 80 | #endregion 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Urls 2 | 3 | Treat Urls as first-class citizens 4 | 5 | [![.NET](https://github.com/MelbourneDeveloper/Urls/actions/workflows/dotnet.yml/badge.svg?branch=main)](https://github.com/MelbourneDeveloper/Urls/actions/workflows/dotnet.yml) 6 | 7 | Nuget: [Urls](https://www.nuget.org/packages/Urls) 8 | 9 | | .NET Framework 4.5 | .NET Standard 2.0 | .NET Core 5.0 | 10 | |--------------------|:-----------------:|---------------| 11 | 12 | Urls is a .NET library of [records](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/records) that represent Urls. All properties are immutable, and there are a collection of Fluent API style extension methods to make Url construction easy. I designed this library with F# in mind. Use the [non-destructive mutation](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/records#non-destructive-mutation) (`with`) syntax to create new Urls easily and make HTTP calls with [RestClient.Net](https://github.com/MelbourneDeveloper/RestClient.Net/tree/5/develop). 13 | 14 | See all samples in the unit tests [here](https://github.com/MelbourneDeveloper/Urls/blob/ab57a866d27cb5653b97ca6fcf8fe51242d5b274/src/Uris.Tests/UriTests.cs#L38). 15 | 16 | #### C# 17 | 18 | ```cs 19 | private readonly string expected = $"{Scheme}://{Username}:{Password}@{Host}:{Port}/{PathPart1}/{PathPart2}?" + 20 | $"{FieldName1}={FieldValueEncoded1}&{FieldName2}={FieldValueEncoded2}#{Fragment}"; 21 | 22 | [TestMethod] 23 | public void TestComposition() 24 | { 25 | var absoluteUrl = 26 | Host.ToHttpUrlFromHost(Port) 27 | .AddQueryParameter(FieldName1, FieldValue1) 28 | .WithCredentials(Username, Password) 29 | .AddQueryParameter(FieldName2, FieldValue2) 30 | .WithFragment(Fragment) 31 | .WithPath(PathPart1, PathPart2); 32 | 33 | Assert.AreEqual( 34 | expected, 35 | absoluteUrl.ToString()); 36 | 37 | //C# 9 records non-destructive mutation (with syntax) 38 | var absoluteUrl2 = absoluteUrl with { Port = 1000 }; 39 | 40 | Assert.AreEqual(1000, absoluteUrl2.Port); 41 | } 42 | ``` 43 | 44 | #### F# 45 | 46 | ```fs 47 | [] 48 | member this.TestComposition () = 49 | 50 | let uri = 51 | "host.com".ToHttpUrlFromHost(5000) 52 | .AddQueryParameter("fieldname1", "field<>Value1") 53 | .WithCredentials("username", "password") 54 | .AddQueryParameter("FieldName2", "field<>Value2") 55 | .WithFragment("frag") 56 | .WithPath("pathpart1", "pathpart2") 57 | 58 | Assert.AreEqual("http://username:password@host.com:5000/pathpart1/pathpart2?fieldname1=field%3C%3EValue1&FieldName2=field%3C%3EValue2#frag",uri.ToString()); 59 | ``` 60 | 61 | #### Pass `AbsoluteUrl` as `System.Uri` 62 | 63 | Automatically convert between `System.Uri` and back 64 | 65 | ```cs 66 | public static HttpClient GetHttpClientWithAbsoluteUrl 67 | => GetHttpClient(new AbsoluteUrl("http", "host.com") 68 | .AddQueryParameter(FieldName1, FieldValue1)); 69 | 70 | public static HttpClient GetHttpClient(Uri uri) => new() { BaseAddress = uri }; 71 | 72 | public static Uri GetUri() => new AbsoluteUrl("http", "host.com").ToUri(); 73 | ``` 74 | 75 | #### Quality First 76 | 77 | ![Code Coverage](https://github.com/MelbourneDeveloper/Urls/blob/main/Images/CodeCoverage.png) 78 | ![Mutation Score](https://github.com/MelbourneDeveloper/Urls/blob/main/Images/MutationScore.png) 79 | 80 | -------------------------------------------------------------------------------- /src/Uris/UrlExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | 7 | #pragma warning disable IDE0057 // Use range operator 8 | 9 | [assembly: InternalsVisibleTo("Urls.Tests")] 10 | 11 | namespace Urls; 12 | 13 | public static class UrlExtensions 14 | { 15 | internal const string ErrorMessageMustBeAbsolute = 16 | "The Uri must be an absolute Uri even when converting to a RelativeUrl"; 17 | 18 | public static Uri ToUri(this AbsoluteUrl url) => new(url.ToString()); 19 | 20 | public static AbsoluteUrl ToAbsoluteUrl(this Uri uri) 21 | { 22 | if (!uri.IsAbsoluteUri) 23 | throw new InvalidOperationException(ErrorMessageMustBeAbsolute); 24 | 25 | var userInfoTokens = uri.UserInfo?.Split([':'], StringSplitOptions.RemoveEmptyEntries); 26 | 27 | var relativeUrl = ToRelativeUrl(uri); 28 | 29 | var userInfo = 30 | userInfoTokens != null && userInfoTokens.Length > 0 31 | ? new UserInfo( 32 | userInfoTokens.First(), 33 | userInfoTokens.Length > 1 ? userInfoTokens[1] : "" 34 | ) 35 | : UserInfo.Empty; 36 | 37 | return new AbsoluteUrl(uri.Scheme, uri.Host, uri.Port, relativeUrl, userInfo); 38 | } 39 | 40 | public static RelativeUrl ToRelativeUrl(this Uri uri) 41 | { 42 | if (!uri.IsAbsoluteUri) 43 | throw new InvalidOperationException(ErrorMessageMustBeAbsolute); 44 | 45 | var path = ImmutableList.Create( 46 | uri.LocalPath.Split(['/'], StringSplitOptions.RemoveEmptyEntries) 47 | ); 48 | 49 | var queryParametersList = new List(); 50 | 51 | var queryParameterTokens = new string[0]; 52 | if (uri.Query.Length >= 1) 53 | { 54 | queryParameterTokens = uri.Query.Substring(1).Split(['&']); 55 | } 56 | 57 | queryParametersList.AddRange( 58 | queryParameterTokens 59 | .Select(keyValueString => keyValueString.Split(['='])) 60 | .Select(keyAndValue => new QueryParameter( 61 | keyAndValue.First(), 62 | keyAndValue.Length > 1 ? keyAndValue[1] : null 63 | )) 64 | ); 65 | 66 | var fragment = uri.Fragment.Length >= 1 ? uri.Fragment.Substring(1) : ""; 67 | 68 | return new RelativeUrl( 69 | path, 70 | queryParametersList.Count == 0 ? [] : queryParametersList.ToImmutableList(), 71 | fragment 72 | ); 73 | } 74 | 75 | //TODO: this looks mighty similar to the above. How to merge? 76 | 77 | public static RelativeUrl ToRelativeUrl(this string relativeUrlString) 78 | { 79 | if (string.IsNullOrEmpty(relativeUrlString)) 80 | return RelativeUrl.Empty; 81 | 82 | var tokens = relativeUrlString.Split(['#'], StringSplitOptions.None); 83 | 84 | var fragment = tokens.Length > 1 ? tokens[1] : ""; 85 | 86 | tokens = tokens[0].Split(['?'], StringSplitOptions.None); 87 | 88 | var queryString = tokens.Length > 1 ? tokens[1] : ""; 89 | 90 | var pathString = tokens[0]; 91 | 92 | var path = ImmutableList.Create( 93 | pathString 94 | .Split(['/'], StringSplitOptions.None) 95 | .Where(s => !string.IsNullOrWhiteSpace(s)) 96 | .ToArray() 97 | ); 98 | 99 | var queryParametersList = 100 | queryString.Length >= 1 101 | ? 102 | [ 103 | .. queryString 104 | .Split(['&']) 105 | .Select(keyValueString => keyValueString.Split(['='])) 106 | .Select(keyAndValue => new QueryParameter( 107 | keyAndValue.First(), 108 | keyAndValue.Length > 1 ? keyAndValue[1] : null 109 | )), 110 | ] 111 | : new List(); 112 | 113 | return new RelativeUrl( 114 | path, 115 | queryParametersList.Count == 0 116 | ? ImmutableList.Empty 117 | : queryParametersList.ToImmutableList(), 118 | fragment 119 | ); 120 | } 121 | 122 | public static AbsoluteUrl WithRelativeUrl( 123 | this AbsoluteUrl absoluteUrl, 124 | RelativeUrl relativeUrl 125 | ) => 126 | absoluteUrl == null 127 | ? throw new ArgumentNullException(nameof(absoluteUrl)) 128 | : new AbsoluteUrl( 129 | absoluteUrl.Scheme, 130 | absoluteUrl.Host, 131 | absoluteUrl.Port, 132 | relativeUrl, 133 | absoluteUrl.UserInfo 134 | ); 135 | 136 | public static RelativeUrl WithFragment(this RelativeUrl relativeUrl, string fragment) => 137 | relativeUrl == null 138 | ? throw new ArgumentNullException(nameof(relativeUrl)) 139 | : new(relativeUrl.Path, relativeUrl.QueryParameters, fragment); 140 | 141 | public static RelativeUrl AddQueryString( 142 | this RelativeUrl relativeUrl, 143 | string fieldName, 144 | string value 145 | ) => 146 | relativeUrl == null 147 | ? throw new ArgumentNullException(nameof(relativeUrl)) 148 | : relativeUrl with 149 | { 150 | QueryParameters = relativeUrl.QueryParameters.Add( 151 | new QueryParameter(fieldName, value) 152 | ), 153 | }; 154 | 155 | public static AbsoluteUrl AddQueryParameter( 156 | this AbsoluteUrl absoluteUrl, 157 | string fieldName, 158 | string value 159 | ) => 160 | absoluteUrl == null 161 | ? throw new ArgumentNullException(nameof(absoluteUrl)) 162 | : absoluteUrl with 163 | { 164 | RelativeUrl = absoluteUrl.RelativeUrl with 165 | { 166 | QueryParameters = absoluteUrl.RelativeUrl.QueryParameters.Add( 167 | new QueryParameter(fieldName, value) 168 | ), 169 | }, 170 | }; 171 | 172 | public static QueryParameter ToQueryParameter(this string fieldName, string value) => 173 | new(fieldName, value); 174 | 175 | public static RelativeUrl WithQueryParameters(this RelativeUrl relativeUrl, T item) => 176 | relativeUrl == null 177 | ? throw new ArgumentNullException(nameof(relativeUrl)) 178 | : relativeUrl with 179 | { 180 | QueryParameters = typeof(T) 181 | .GetProperties() 182 | .Select(propertyInfo => new QueryParameter( 183 | propertyInfo.Name, 184 | propertyInfo.GetValue(item)?.ToString() 185 | )) 186 | .ToImmutableList(), 187 | }; 188 | 189 | public static AbsoluteUrl WithCredentials( 190 | this AbsoluteUrl absoluteUrl, 191 | string username, 192 | string password 193 | ) => 194 | absoluteUrl == null 195 | ? throw new ArgumentNullException(nameof(absoluteUrl)) 196 | : absoluteUrl with 197 | { 198 | UserInfo = new(username, password), 199 | }; 200 | 201 | public static AbsoluteUrl WithFragment(this AbsoluteUrl absoluteUrl, string fragment) => 202 | absoluteUrl == null 203 | ? throw new ArgumentNullException(nameof(absoluteUrl)) 204 | : absoluteUrl with 205 | { 206 | RelativeUrl = absoluteUrl.RelativeUrl with { Fragment = fragment }, 207 | }; 208 | 209 | public static AbsoluteUrl WithPath( 210 | this AbsoluteUrl absoluteUrl, 211 | IReadOnlyList pathSegments 212 | ) => 213 | absoluteUrl == null 214 | ? throw new ArgumentNullException(nameof(absoluteUrl)) 215 | : absoluteUrl with 216 | { 217 | RelativeUrl = absoluteUrl.RelativeUrl with 218 | { 219 | Path = pathSegments.ToImmutableList(), 220 | }, 221 | }; 222 | 223 | public static AbsoluteUrl WithPath( 224 | this AbsoluteUrl absoluteUrl, 225 | params string[] pathSegments 226 | ) => WithPath(absoluteUrl, pathSegments.ToList()); 227 | 228 | public static AbsoluteUrl WithPort(this AbsoluteUrl absoluteUrl, int port) => 229 | absoluteUrl == null 230 | ? throw new ArgumentNullException(nameof(absoluteUrl)) 231 | : absoluteUrl with 232 | { 233 | Port = port, 234 | }; 235 | 236 | public static AbsoluteUrl ToHttpUrlFromHost(this string host, int? port = null) => 237 | host == null 238 | ? throw new ArgumentNullException(nameof(host)) 239 | : new AbsoluteUrl("http", host, port); 240 | 241 | public static AbsoluteUrl ToHttpsUrlFromHost(this string host, int? port = null) => 242 | host == null 243 | ? throw new ArgumentNullException(nameof(host)) 244 | : new AbsoluteUrl("https", host, port); 245 | 246 | public static AbsoluteUrl ToAbsoluteUrl(this string urlString) => 247 | new Uri(urlString, UriKind.Absolute).ToAbsoluteUrl(); 248 | 249 | public static RelativeUrl AppendPath(this RelativeUrl relativeUrl, params string[] args) => 250 | relativeUrl == null 251 | ? throw new ArgumentNullException(nameof(relativeUrl)) 252 | : relativeUrl with 253 | { 254 | Path = relativeUrl.Path.AddRange(args), 255 | }; 256 | 257 | public static AbsoluteUrl AppendPath(this AbsoluteUrl absoluteUrl, params string[] args) => 258 | absoluteUrl == null 259 | ? throw new ArgumentNullException(nameof(absoluteUrl)) 260 | : absoluteUrl with 261 | { 262 | RelativeUrl = absoluteUrl.RelativeUrl.AppendPath(args), 263 | }; 264 | 265 | public static ImmutableList ToQueryParameters( 266 | this QueryParameter queryParameter 267 | ) => ImmutableList.Create(queryParameter); 268 | } 269 | -------------------------------------------------------------------------------- /src/Uris.Tests/UriTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Net; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | 9 | namespace Urls.Tests; 10 | 11 | [TestClass] 12 | public class UrlTests 13 | { 14 | private const string Scheme = "http"; 15 | private const string Host = "host.com"; 16 | private const int Port = 5000; 17 | private const string PathPart1 = "pathpart1"; 18 | private const string PathPart2 = "pathpart2"; 19 | private const string FieldName1 = "fieldname1"; 20 | private const string FieldName2 = "FieldName2"; 21 | private const string FieldValue1 = "field<>Value1"; 22 | private const string FieldValue2 = "field<>Value2"; 23 | private const string FieldValueEncoded1 = "field%3C%3EValue1"; 24 | private const string FieldValueEncoded2 = "field%3C%3EValue2"; 25 | private const string Fragment = "frag"; 26 | private const string Username = "username"; 27 | private const string Password = "password"; 28 | 29 | private readonly string expected = 30 | $"{Scheme}://{Username}:{Password}@{Host}:{Port}/{PathPart1}/{PathPart2}?" 31 | + $"{FieldName1}={FieldValueEncoded1}&{FieldName2}={FieldValueEncoded2}#{Fragment}"; 32 | 33 | [TestMethod] 34 | public void TestQueryParameterEquality() 35 | { 36 | var qp1 = new QueryParameter("n", "v"); 37 | var qp2 = new QueryParameter("n", "v"); 38 | Assert.AreEqual(qp1, qp2); 39 | } 40 | 41 | [TestMethod] 42 | public void TestEquality() 43 | { 44 | var absoluteUrl1 = expected.ToAbsoluteUrl(); 45 | Uri uri = absoluteUrl1; 46 | var uri2 = absoluteUrl1.ToUri(); 47 | var absoluteUrl2 = (AbsoluteUrl)uri; 48 | 49 | Assert.AreEqual(uri, absoluteUrl1); 50 | Assert.AreEqual(uri.ToString(), WebUtility.UrlDecode(absoluteUrl2.ToString())); 51 | Assert.AreEqual(WebUtility.UrlDecode(absoluteUrl1.ToString()), uri.ToString()); 52 | Assert.AreEqual(absoluteUrl1.ToString(), absoluteUrl2.ToString()); 53 | 54 | //Just here to get more granular 55 | Assert.AreEqual(absoluteUrl1.UserInfo, absoluteUrl2.UserInfo); 56 | Assert.IsTrue( 57 | absoluteUrl1.RelativeUrl.QueryParameters.SequenceEqual( 58 | absoluteUrl2.RelativeUrl.QueryParameters 59 | ) 60 | ); 61 | Assert.AreEqual(absoluteUrl1.RelativeUrl, absoluteUrl2.RelativeUrl); 62 | 63 | Assert.AreEqual( 64 | absoluteUrl1.RelativeUrl.GetHashCode(), 65 | absoluteUrl2.RelativeUrl.GetHashCode() 66 | ); 67 | 68 | Assert.AreEqual(absoluteUrl1, absoluteUrl2); 69 | Assert.AreEqual(absoluteUrl2, uri); 70 | Assert.AreEqual(absoluteUrl2, absoluteUrl1); 71 | 72 | Assert.AreEqual(uri, uri2); 73 | } 74 | 75 | [TestMethod] 76 | public void Test() 77 | { 78 | var uriString = new AbsoluteUrl( 79 | Scheme, 80 | Host, 81 | Port, 82 | new RelativeUrl( 83 | ImmutableList.Create(PathPart1, PathPart2), 84 | ImmutableList.Create( 85 | new QueryParameter(FieldName1, FieldValue1), 86 | new QueryParameter(FieldName2, FieldValue2) 87 | ), 88 | Fragment 89 | ), 90 | new UserInfo(Username, Password) 91 | ).ToString(); 92 | 93 | Assert.AreEqual(expected, uriString); 94 | } 95 | 96 | [TestMethod] 97 | public void TestComposition2() 98 | { 99 | var url = new AbsoluteUrl(Scheme, Host, Port) 100 | .AddQueryParameter(FieldName1, FieldValue1) 101 | .WithCredentials(Username, Password) 102 | .AddQueryParameter(FieldName2, FieldValue2) 103 | .WithFragment(Fragment) 104 | .WithPath(PathPart1, PathPart2); 105 | 106 | Assert.AreEqual(expected, url.ToString()); 107 | } 108 | 109 | [TestMethod] 110 | public void TestLocalFunctionToUri() 111 | { 112 | static AbsoluteUrl SomeFunctionTakingAUri(Uri uri) => uri.ToAbsoluteUrl(); 113 | var absoluteUrl = "www.test.com".ToHttpsUrlFromHost().WithPort(443); 114 | Assert.AreEqual(absoluteUrl, SomeFunctionTakingAUri(absoluteUrl)); 115 | } 116 | 117 | [TestMethod] 118 | public void TestComposition() 119 | { 120 | var absoluteUrl = Host.ToHttpUrlFromHost(Port) 121 | .AddQueryParameter(FieldName1, FieldValue1) 122 | .WithCredentials(Username, Password) 123 | .AddQueryParameter(FieldName2, FieldValue2) 124 | .WithFragment(Fragment) 125 | .WithPath(PathPart1, PathPart2); 126 | 127 | Assert.AreEqual(expected, absoluteUrl.ToString()); 128 | 129 | //C# 9 records non-destructive mutation (with syntax) 130 | var absoluteUrl2 = absoluteUrl with 131 | { 132 | Port = 1000, 133 | }; 134 | 135 | Assert.AreEqual(1000, absoluteUrl2.Port); 136 | } 137 | 138 | [TestMethod] 139 | public void TestComposition3() 140 | { 141 | var uri = Host.ToHttpsUrlFromHost(); 142 | 143 | Assert.AreEqual($"https://{Host}", uri.ToString()); 144 | } 145 | 146 | [TestMethod] 147 | public void TestAbsoluteWithRelative() 148 | { 149 | var absolute = new AbsoluteUrl(Scheme, Host); 150 | 151 | var relativeRelativeUrl = new RelativeUrl( 152 | ImmutableList.Create(PathPart1, PathPart2), 153 | ImmutableList.Create( 154 | new QueryParameter(FieldName1, FieldValue1), 155 | new QueryParameter(FieldName2, FieldValue2) 156 | ) 157 | ); 158 | 159 | absolute = absolute.WithRelativeUrl(relativeRelativeUrl); 160 | 161 | Assert.AreEqual(relativeRelativeUrl.Fragment, absolute.RelativeUrl.Fragment); 162 | } 163 | 164 | [TestMethod] 165 | public void TestRelativeWithFragment() 166 | { 167 | var relativeRelativeUrl = new RelativeUrl( 168 | ImmutableList.Create(PathPart1, PathPart2), 169 | ImmutableList.Create( 170 | new QueryParameter(FieldName1, FieldValue1), 171 | new QueryParameter(FieldName2, FieldValue2) 172 | ) 173 | ); 174 | 175 | const string frag = "test"; 176 | 177 | relativeRelativeUrl = relativeRelativeUrl.WithFragment(frag); 178 | 179 | Assert.AreEqual(frag, relativeRelativeUrl.Fragment); 180 | } 181 | 182 | #pragma warning disable IDE0034 // Simplify 'default' expression 183 | #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. 184 | [TestMethod] 185 | public void TestRelativeWithNullFragment() 186 | { 187 | var relativeRelativeUrl = new RelativeUrl( 188 | ImmutableList.Create(PathPart1, PathPart2), 189 | QueryParameter.EmptyList, 190 | default(string) 191 | ); 192 | Assert.AreEqual("", relativeRelativeUrl.Fragment); 193 | 194 | relativeRelativeUrl = new RelativeUrl( 195 | ImmutableList.Create(PathPart1, PathPart2), 196 | QueryParameter.EmptyList, 197 | null 198 | ); 199 | Assert.AreEqual("", relativeRelativeUrl.Fragment); 200 | 201 | relativeRelativeUrl = new RelativeUrl(default(RelativeUrl)); 202 | Assert.AreEqual("", relativeRelativeUrl.Fragment); 203 | } 204 | #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. 205 | #pragma warning restore IDE0034 // Simplify 'default' expression 206 | 207 | [TestMethod] 208 | public void TestWithQueryStringStrings() 209 | { 210 | var relativeRelativeUrl = RelativeUrl.Empty.AddQueryString(FieldName1, FieldValue1); 211 | 212 | Assert.AreEqual(FieldName1, relativeRelativeUrl.QueryParameters?.First().FieldName); 213 | 214 | Assert.AreEqual(FieldValue1, relativeRelativeUrl.QueryParameters?.First().Value); 215 | } 216 | 217 | [TestMethod] 218 | public void TestAbsoluteWithQueryStringStrings() 219 | { 220 | var absoluteRelativeUrl = new AbsoluteUrl("https", "test.com"); 221 | 222 | absoluteRelativeUrl = absoluteRelativeUrl.AddQueryParameter(FieldName1, FieldValue1); 223 | 224 | Assert.AreEqual( 225 | FieldName1, 226 | absoluteRelativeUrl.RelativeUrl.QueryParameters.First().FieldName 227 | ); 228 | 229 | Assert.AreEqual(FieldValue1, absoluteRelativeUrl.RelativeUrl.QueryParameters.First().Value); 230 | } 231 | 232 | [TestMethod] 233 | public void TestMinimalAbsoluteToString() => 234 | Assert.AreEqual("https://test.com", new AbsoluteUrl("https", "test.com").ToString()); 235 | 236 | [TestMethod] 237 | public void TestConstructUri() 238 | { 239 | var uriString = new AbsoluteUrl( 240 | Scheme, 241 | Host, 242 | Port, 243 | new RelativeUrl( 244 | ImmutableList.Create(PathPart1, PathPart2), 245 | ImmutableList.Create( 246 | new QueryParameter(FieldName1, FieldValue1), 247 | new QueryParameter(FieldName2, FieldValue2) 248 | ), 249 | Fragment 250 | ), 251 | new UserInfo(Username, Password) 252 | ).ToString(); 253 | 254 | var uri = new Uri(uriString, UriKind.Absolute); 255 | 256 | Assert.IsNotNull(uri); 257 | Assert.AreEqual(uri.Scheme, Scheme); 258 | } 259 | 260 | [TestMethod] 261 | public void TestWithQueryParams() 262 | { 263 | var item = new 264 | { 265 | somelongstring = "gvhhvhgfgfdg7676878", 266 | count = 50, 267 | message = "This is a sentence", 268 | }; 269 | 270 | var relativeUrl = RelativeUrl.Empty.WithQueryParameters(item); 271 | 272 | Assert.AreEqual(item.somelongstring, relativeUrl.QueryParameters[0].Value); 273 | Assert.AreEqual(nameof(item.somelongstring), relativeUrl.QueryParameters[0].FieldName); 274 | Assert.AreEqual(item.count.ToString(CultureInfo.InvariantCulture), relativeUrl.QueryParameters[1].Value); 275 | Assert.AreEqual(nameof(item.count), relativeUrl.QueryParameters[1].FieldName); 276 | Assert.AreEqual(item.message, relativeUrl.QueryParameters[2].Value); 277 | Assert.AreEqual(nameof(item.message), relativeUrl.QueryParameters[2].FieldName); 278 | } 279 | 280 | [TestMethod] 281 | public void TestFromUri() 282 | { 283 | var uriString = new AbsoluteUrl( 284 | Scheme, 285 | Host, 286 | Port, 287 | new RelativeUrl( 288 | ImmutableList.Create(PathPart1, PathPart2), 289 | ImmutableList.Create( 290 | new QueryParameter(FieldName1, FieldValue1), 291 | new QueryParameter(FieldName2, FieldValue2) 292 | ), 293 | Fragment 294 | ), 295 | new UserInfo(Username, Password) 296 | ).ToString(); 297 | 298 | var uri = new Uri(uriString, UriKind.Absolute).ToAbsoluteUrl(); 299 | 300 | Assert.IsNotNull(uri); 301 | Assert.AreEqual(uri.Scheme, Scheme); 302 | Assert.AreEqual(uri.RelativeUrl.Fragment, Fragment); 303 | Assert.AreEqual(uri.RelativeUrl.QueryParameters.First().FieldName, FieldName1); 304 | Assert.AreEqual(uri.RelativeUrl.QueryParameters.First().Value, FieldValue1); 305 | Assert.AreEqual(uri.RelativeUrl.QueryParameters[1].FieldName, FieldName2); 306 | Assert.AreEqual(uri.RelativeUrl.QueryParameters[1].Value, FieldValue2); 307 | Assert.AreEqual(Host, uri.Host); 308 | Assert.AreEqual(Port, uri.Port); 309 | Assert.AreEqual(PathPart1, uri.RelativeUrl.Path[0]); 310 | Assert.AreEqual(PathPart2, uri.RelativeUrl.Path[1]); 311 | Assert.AreEqual(Fragment, uri.RelativeUrl.Fragment); 312 | Assert.AreEqual(Username, uri.UserInfo?.Username); 313 | Assert.AreEqual(Password, uri.UserInfo?.Password); 314 | } 315 | 316 | [TestMethod] 317 | public void TestRelativeToAbsoluteUrlThrowsException() => 318 | Assert.AreEqual( 319 | UrlExtensions.ErrorMessageMustBeAbsolute, 320 | Assert 321 | .ThrowsException( 322 | () => new Uri("", UriKind.Relative).ToAbsoluteUrl() 323 | ) 324 | .Message 325 | ); 326 | 327 | [TestMethod] 328 | public void TestRelativeToRelativeUrlThrowsException() => 329 | Assert.AreEqual( 330 | UrlExtensions.ErrorMessageMustBeAbsolute, 331 | Assert 332 | .ThrowsException( 333 | () => new Uri("", UriKind.Relative).ToRelativeUrl() 334 | ) 335 | .Message 336 | ); 337 | 338 | [TestMethod] 339 | public void TestRelativeUrl() 340 | { 341 | var RelativeUrl = "/".ToRelativeUrl(); 342 | Assert.AreEqual(string.Empty, RelativeUrl.ToString()); 343 | Assert.IsTrue(RelativeUrl.QueryParameters.Count == 0); 344 | } 345 | 346 | [TestMethod] 347 | public void TestRelativeUrlConstructors() 348 | { 349 | var RelativeUrl = "a/a".ToRelativeUrl(); 350 | Assert.IsTrue(RelativeUrl.Path.SequenceEqual(["a", "a"])); 351 | 352 | RelativeUrl = "a/".ToRelativeUrl(); 353 | Assert.IsTrue(RelativeUrl.Path.SequenceEqual(["a"])); 354 | 355 | RelativeUrl = "a/b/c".ToRelativeUrl(); 356 | Assert.IsTrue(RelativeUrl.Path.SequenceEqual(["a", "b", "c"])); 357 | 358 | RelativeUrl = "a/b/c/".ToRelativeUrl(); 359 | Assert.IsTrue(RelativeUrl.Path.SequenceEqual(["a", "b", "c"])); 360 | 361 | RelativeUrl = "".ToRelativeUrl(); 362 | Assert.IsTrue(RelativeUrl.Path.SequenceEqual([])); 363 | 364 | RelativeUrl = "?asd=1".ToRelativeUrl(); 365 | Assert.AreEqual("asd", RelativeUrl.QueryParameters[0].FieldName); 366 | Assert.AreEqual("1", RelativeUrl.QueryParameters[0].Value); 367 | 368 | RelativeUrl = "a/a?a=1#a".ToRelativeUrl(); 369 | Assert.AreEqual("a", RelativeUrl.Path[0]); 370 | Assert.AreEqual("a", RelativeUrl.Path[1]); 371 | Assert.AreEqual("a", RelativeUrl.QueryParameters[0].FieldName); 372 | Assert.AreEqual("1", RelativeUrl.QueryParameters[0].Value); 373 | Assert.AreEqual("a", RelativeUrl.Fragment); 374 | 375 | RelativeUrl = "a/a?a=#a".ToRelativeUrl(); 376 | Assert.AreEqual("a", RelativeUrl.Path[0]); 377 | Assert.AreEqual("a", RelativeUrl.Path[1]); 378 | Assert.AreEqual("a", RelativeUrl.QueryParameters[0].FieldName); 379 | Assert.AreEqual("", RelativeUrl.QueryParameters[0].Value); 380 | Assert.AreEqual("a", RelativeUrl.Fragment); 381 | 382 | RelativeUrl = new RelativeUrl("a/a?a=#a"); 383 | Assert.AreEqual("a", RelativeUrl.Path[0]); 384 | Assert.AreEqual("a", RelativeUrl.Path[1]); 385 | Assert.AreEqual("a", RelativeUrl.QueryParameters[0].FieldName); 386 | Assert.AreEqual("", RelativeUrl.QueryParameters[0].Value); 387 | Assert.AreEqual("a", RelativeUrl.Fragment); 388 | 389 | RelativeUrl = "a/a?a#a".ToRelativeUrl(); 390 | Assert.AreEqual("a", RelativeUrl.Path[0]); 391 | Assert.AreEqual("a", RelativeUrl.Path[1]); 392 | Assert.AreEqual("a", RelativeUrl.QueryParameters[0].FieldName); 393 | Assert.AreEqual(null, RelativeUrl.QueryParameters[0].Value); 394 | Assert.AreEqual("a", RelativeUrl.Fragment); 395 | Assert.AreEqual("/a/a?a#a", RelativeUrl.ToString()); 396 | } 397 | 398 | [TestMethod] 399 | public void TestToAbsoluteUrlThings() 400 | { 401 | var absoluteUrl = new AbsoluteUrl($"{Scheme}://{Host}"); 402 | 403 | Assert.AreEqual(Scheme, absoluteUrl.Scheme); 404 | Assert.AreEqual(Host, absoluteUrl.Host); 405 | 406 | absoluteUrl = new AbsoluteUrl($"{Scheme}://{Host}:{Port}"); 407 | Assert.AreEqual(Port, absoluteUrl.Port); 408 | 409 | absoluteUrl = new AbsoluteUrl("http://www.hotmail.com"); 410 | Assert.AreEqual("www.hotmail.com", absoluteUrl.Host); 411 | 412 | absoluteUrl = new AbsoluteUrl("http://bob:@www.hotmail.com"); 413 | Assert.AreEqual("bob", absoluteUrl.UserInfo.Username); 414 | } 415 | 416 | [TestMethod] 417 | public void TestWithRelative() 418 | { 419 | const string urlString = "https://localhost:44337/JsonPerson"; 420 | var baseUri = new AbsoluteUrl(urlString); 421 | var completeUri = baseUri.WithRelativeUrl( 422 | baseUri.RelativeUrl.AddQueryString("personKey", "abc") 423 | ); 424 | Assert.AreEqual($"{urlString}?personKey=abc", completeUri.ToString()); 425 | } 426 | 427 | [TestMethod] 428 | public void TestWithRelative2() 429 | { 430 | var relativeUrl = "/JsonPerson?personKey=123".ToRelativeUrl(); 431 | Assert.AreEqual("personKey", relativeUrl.QueryParameters.First().FieldName); 432 | } 433 | 434 | [TestMethod] 435 | public void TestAppendPath() 436 | { 437 | const string urlString = "http://www.test.com:80/test"; 438 | var baseUri = new AbsoluteUrl(urlString); 439 | var completeUri = baseUri.AppendPath("test"); 440 | Assert.AreEqual($"http://www.test.com:80/test/test", completeUri.ToString()); 441 | } 442 | 443 | [TestMethod] 444 | public void TestUserInfoStuff() => 445 | Assert.IsTrue( 446 | (new UserInfo("a", "b") with { Username = "b" }).Equals(new UserInfo("b", "b")) 447 | ); 448 | 449 | #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. 450 | #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. 451 | [TestMethod] 452 | public void TestToAbsoluteUriNullGuard() => 453 | Assert.ThrowsException(() => ((Uri)null).ToAbsoluteUrl()); 454 | 455 | [TestMethod] 456 | public void TestToRelativeUriNullGuard() => 457 | Assert.ThrowsException(() => ((Uri)null).ToRelativeUrl()); 458 | 459 | [TestMethod] 460 | public void TestToRelativeUriNullGuard2() => 461 | Assert.ThrowsException(() => ((string)null).ToRelativeUrl()); 462 | 463 | [TestMethod] 464 | public void TestAbsoluteUrlNullIsEmpty() => 465 | Assert.AreEqual(AbsoluteUrl.Empty, new AbsoluteUrl((AbsoluteUrl)null)); 466 | 467 | [TestMethod] 468 | public void TestRelativeUrlNullIsEmpty() => 469 | Assert.AreEqual(RelativeUrl.Empty, new RelativeUrl((RelativeUrl)null)); 470 | 471 | [TestMethod] 472 | public void TestUserInfoNullIsEmpty() => Assert.AreEqual(UserInfo.Empty, new UserInfo(null)); 473 | #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. 474 | #pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. 475 | 476 | [TestMethod] 477 | public void TestToQueryParameter() => 478 | "a".ToQueryParameter("b").Equals(new QueryParameter("a", "b")); 479 | 480 | [TestMethod] 481 | public void TestEmpty() 482 | { 483 | Assert.AreEqual("://", AbsoluteUrl.Empty.ToString()); 484 | Assert.AreEqual("", UserInfo.Empty.ToString()); 485 | Assert.AreEqual("", RelativeUrl.Empty.ToString()); 486 | Assert.AreEqual(0, QueryParameter.EmptyList.Count); 487 | } 488 | 489 | [TestMethod] 490 | public void TestToQueryParameters() => 491 | Assert.AreEqual("a", new QueryParameter("a", "a").ToQueryParameters()[0].FieldName); 492 | 493 | [TestMethod] 494 | public void TestCanUseWith() 495 | { 496 | const string bee = "b"; 497 | const string ay = "a"; 498 | var queryParameter = new QueryParameter(ay, ay) with { FieldName = bee, Value = " { bee }.ToImmutableList(), 507 | QueryParameters = queryParameter.ToQueryParameters(), 508 | }; 509 | Assert.AreEqual(bee, relativeUrl.Fragment); 510 | Assert.IsTrue( 511 | queryParameter.ToQueryParameters().SequenceEqual(relativeUrl.QueryParameters) 512 | ); 513 | } 514 | 515 | [TestMethod] 516 | public void TestWithRelativeUrlShouldAppendNotReplacePath() 517 | { 518 | // Bug: WithRelativeUrl() replaces the base path instead of appending to it 519 | // This test demonstrates the issue with API base URLs that contain paths 520 | 521 | // Test case 1: Swagger Petstore API example 522 | var baseUrl = "https://petstore3.swagger.io/api/v3".ToAbsoluteUrl(); 523 | var relativeUrl = new RelativeUrl("/pet/1"); 524 | var combined = baseUrl.WithRelativeUrl(relativeUrl); 525 | 526 | // Expected: base path /api/v3 should be preserved and /pet/1 appended to it 527 | // Actual (buggy): base path /api/v3 gets replaced by /pet/1 528 | Assert.AreEqual("https://petstore3.swagger.io/api/v3/pet/1", combined.ToString()); 529 | 530 | // Test case 2: Versioned API example 531 | var apiBase = "https://api.example.com/v2".ToAbsoluteUrl(); 532 | var endpoint = new RelativeUrl("/users/123"); 533 | var result = apiBase.WithRelativeUrl(endpoint); 534 | 535 | Assert.AreEqual("https://api.example.com/v2/users/123", result.ToString()); 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # IDE0160: Convert to block scoped namespace 4 | csharp_style_namespace_declarations = file_scoped 5 | 6 | dotnet_analyzer_diagnostic.category-Usage.severity = error 7 | dotnet_analyzer_diagnostic.category-Reliability.severity = error 8 | dotnet_analyzer_diagnostic.category-Design.severity = error 9 | dotnet_analyzer_diagnostic.category-Naming.severity = error 10 | dotnet_analyzer_diagnostic.category-Maintainability.severity = error 11 | dotnet_analyzer_diagnostic.category-Performance.severity = error 12 | dotnet_analyzer_diagnostic.category-Interoperability.severity = error 13 | dotnet_analyzer_diagnostic.category-Security.severity = error 14 | dotnet_analyzer_diagnostic.category-Globalization.severity = error 15 | dotnet_analyzer_diagnostic.category-Documentation.severity = error 16 | dotnet_analyzer_diagnostic.category-Readability.severity = error 17 | dotnet_analyzer_diagnostic.category-Ordering.severity = error 18 | 19 | # Default severity for all analyzer diagnostics 20 | dotnet_analyzer_diagnostic.severity = error 21 | 22 | # IDE0011: Add braces 23 | csharp_prefer_braces = when_multiline:error 24 | 25 | # IDE0008: Use explicit type 26 | csharp_style_var_elsewhere = true:error 27 | 28 | # IDE0008: Use explicit type 29 | csharp_style_var_for_built_in_types = true:error 30 | 31 | # IDE0008: Use explicit type 32 | csharp_style_var_when_type_is_apparent = true:error 33 | 34 | # IDE0022: Use block body for methods 35 | csharp_style_expression_bodied_methods = when_on_single_line:error 36 | 37 | # IDE0074: Use compound assignment 38 | dotnet_style_prefer_compound_assignment = true:error 39 | 40 | # IDE0023: Use block body for operators 41 | csharp_style_expression_bodied_operators = true:error 42 | 43 | # IDE0021: Use block body for constructors 44 | csharp_style_expression_bodied_constructors = true:error 45 | 46 | # CS1998: Async method lacks 'await' operators and will run synchronously 47 | dotnet_diagnostic.CS1998.severity = error 48 | 49 | # Nullable reference types 50 | dotnet_diagnostic.CS8600.severity = error 51 | dotnet_diagnostic.CS8601.severity = error 52 | dotnet_diagnostic.CS8602.severity = error 53 | dotnet_diagnostic.CS8603.severity = error 54 | dotnet_diagnostic.CS8604.severity = error 55 | dotnet_diagnostic.CS8618.severity = error 56 | dotnet_diagnostic.CS8625.severity = error 57 | dotnet_diagnostic.CS8632.severity = error 58 | dotnet_diagnostic.CS8767.severity = error 59 | 60 | ######################## Disable ################# 61 | 62 | 63 | dotnet_diagnostic.CA1515.severity = None 64 | 65 | 66 | dotnet_diagnostic.IDE0055.severity = None 67 | 68 | # Modify 'Start' to catch a more specific allowed exception type, or rethrow the exception 69 | dotnet_diagnostic.CA1031.severity = None 70 | 71 | # CA1825: Avoid zero-length array allocations 72 | # Not compatible with .NET 45 73 | dotnet_diagnostic.CA1825.severity = none 74 | 75 | # CA1032: Implement standard exception constructors 76 | dotnet_diagnostic.CA1032.severity = None 77 | 78 | ######################## Warnings ################# 79 | 80 | # CS1591: Missing XML comment for publicly visible type or member 81 | dotnet_diagnostic.CS1591.severity = none 82 | 83 | ######################################### 84 | dotnet_diagnostic.IDE0059.severity = error 85 | 86 | 87 | dotnet_diagnostic.CS1573.severity = error 88 | 89 | dotnet_diagnostic.CS0219.severity = error 90 | 91 | dotnet_diagnostic.CS0168.severity = error 92 | 93 | # CA1000: Do not declare static members on generic types 94 | dotnet_diagnostic.CA1000.severity = error 95 | 96 | # CA1001: Types that own disposable fields should be disposable 97 | dotnet_diagnostic.CA1001.severity = error 98 | 99 | # CA1002: Do not expose generic lists 100 | dotnet_diagnostic.CA1002.severity = error 101 | 102 | # CA1003: Use generic event handler instances 103 | dotnet_diagnostic.CA1003.severity = error 104 | 105 | # CA1005: Avoid excessive parameters on generic types 106 | dotnet_diagnostic.CA1005.severity = error 107 | 108 | # CA1008: Enums should have zero value 109 | dotnet_diagnostic.CA1008.severity = error 110 | 111 | # CA1010: Generic interface should also be implemented 112 | dotnet_diagnostic.CA1010.severity = error 113 | 114 | # CA1012: Abstract types should not have public constructors 115 | dotnet_diagnostic.CA1012.severity = error 116 | 117 | # CA1016: Mark assemblies with assembly version 118 | dotnet_diagnostic.CA1016.severity = error 119 | 120 | # CA1017: Mark assemblies with ComVisible 121 | dotnet_diagnostic.CA1017.severity = error 122 | 123 | # CA1018: Mark attributes with AttributeUsageAttribute 124 | dotnet_diagnostic.CA1018.severity = error 125 | 126 | # CA1019: Define accessors for attribute arguments 127 | dotnet_diagnostic.CA1019.severity = error 128 | 129 | # CA1021: Avoid out parameters 130 | dotnet_diagnostic.CA1021.severity = error 131 | 132 | # CA1024: Use properties where appropriate 133 | dotnet_diagnostic.CA1024.severity = error 134 | 135 | # CA1027: Mark enums with FlagsAttribute 136 | dotnet_diagnostic.CA1027.severity = error 137 | 138 | # CA1028: Enum Storage should be Int32 139 | dotnet_diagnostic.CA1028.severity = error 140 | 141 | # CA1030: Use events where appropriate 142 | dotnet_diagnostic.CA1030.severity = error 143 | 144 | # CA1033: Interface methods should be callable by child types 145 | dotnet_diagnostic.CA1033.severity = error 146 | 147 | # CA1034: Nested types should not be visible 148 | dotnet_diagnostic.CA1034.severity = error 149 | 150 | # CA1036: Override methods on comparable types 151 | dotnet_diagnostic.CA1036.severity = error 152 | 153 | # CA1040: Avoid empty interfaces 154 | dotnet_diagnostic.CA1040.severity = error 155 | 156 | # CA1041: Provide ObsoleteAttribute message 157 | dotnet_diagnostic.CA1041.severity = error 158 | 159 | # CA1043: Use Integral Or String Argument For Indexers 160 | dotnet_diagnostic.CA1043.severity = error 161 | 162 | # CA1044: Properties should not be write only 163 | dotnet_diagnostic.CA1044.severity = error 164 | 165 | # CA1045: Do not pass types by reference 166 | dotnet_diagnostic.CA1045.severity = error 167 | 168 | # CA1046: Do not overload equality operator on reference types 169 | dotnet_diagnostic.CA1046.severity = error 170 | 171 | # CA1047: Do not declare protected member in sealed type 172 | dotnet_diagnostic.CA1047.severity = error 173 | 174 | # CA1050: Declare types in namespaces 175 | dotnet_diagnostic.CA1050.severity = error 176 | 177 | # CA1051: Do not declare visible instance fields 178 | dotnet_diagnostic.CA1051.severity = error 179 | 180 | # CA1052: Static holder types should be Static or NotInheritable 181 | dotnet_diagnostic.CA1052.severity = error 182 | 183 | # CA1054: URI-like parameters should not be strings 184 | dotnet_diagnostic.CA1054.severity = error 185 | 186 | # CA1055: URI-like return values should not be strings 187 | dotnet_diagnostic.CA1055.severity = error 188 | 189 | # CA1056: URI-like properties should not be strings 190 | dotnet_diagnostic.CA1056.severity = error 191 | 192 | # CA1058: Types should not extend certain base types 193 | dotnet_diagnostic.CA1058.severity = error 194 | 195 | # CA1060: Move pinvokes to native methods class 196 | dotnet_diagnostic.CA1060.severity = error 197 | 198 | # CA1061: Do not hide base class methods 199 | dotnet_diagnostic.CA1061.severity = error 200 | 201 | # CA1064: Exceptions should be public 202 | dotnet_diagnostic.CA1064.severity = error 203 | 204 | # CA1065: Do not raise exceptions in unexpected locations 205 | dotnet_diagnostic.CA1065.severity = error 206 | 207 | # CA1066: Implement IEquatable when overriding Object.Equals 208 | dotnet_diagnostic.CA1066.severity = error 209 | 210 | # CA1067: Override Object.Equals(object) when implementing IEquatable 211 | dotnet_diagnostic.CA1067.severity = error 212 | 213 | # CA1068: CancellationToken parameters must come last 214 | dotnet_diagnostic.CA1068.severity = error 215 | 216 | # CA1069: Enums values should not be duplicated 217 | dotnet_diagnostic.CA1069.severity = error 218 | 219 | # CA1070: Do not declare event fields as virtual 220 | dotnet_diagnostic.CA1070.severity = error 221 | 222 | # CA1200: Avoid using cref tags with a prefix 223 | dotnet_diagnostic.CA1200.severity = error 224 | 225 | # CA1303: Do not pass literals as localized parameters 226 | dotnet_diagnostic.CA1303.severity = error 227 | 228 | # CA1304: Specify CultureInfo 229 | dotnet_diagnostic.CA1304.severity = error 230 | 231 | # CA1305: Specify IFormatProvider 232 | dotnet_diagnostic.CA1305.severity = error 233 | 234 | # CA1307: Specify StringComparison for clarity 235 | dotnet_diagnostic.CA1307.severity = error 236 | 237 | # CA1308: Normalize strings to uppercase 238 | dotnet_diagnostic.CA1308.severity = error 239 | 240 | # CA1309: Use ordinal string comparison 241 | dotnet_diagnostic.CA1309.severity = error 242 | 243 | # CA1310: Specify StringComparison for correctness 244 | dotnet_diagnostic.CA1310.severity = error 245 | 246 | # CA1401: P/Invokes should not be visible 247 | dotnet_diagnostic.CA1401.severity = error 248 | 249 | # CA1416: Validate platform compatibility 250 | dotnet_diagnostic.CA1416.severity = error 251 | 252 | # CA1417: Do not use 'OutAttribute' on string parameters for P/Invokes 253 | dotnet_diagnostic.CA1417.severity = error 254 | 255 | # CA1501: Avoid excessive inheritance 256 | dotnet_diagnostic.CA1501.severity = error 257 | 258 | # CA1502: Avoid excessive complexity 259 | dotnet_diagnostic.CA1502.severity = error 260 | 261 | # CA1505: Avoid unmaintainable code 262 | dotnet_diagnostic.CA1505.severity = error 263 | 264 | # CA1506: Avoid excessive class coupling 265 | dotnet_diagnostic.CA1506.severity = error 266 | 267 | # CA1507: Use nameof to express symbol names 268 | dotnet_diagnostic.CA1507.severity = error 269 | 270 | # CA1508: Avoid dead conditional code 271 | dotnet_diagnostic.CA1508.severity = error 272 | 273 | # CA1509: Invalid entry in code metrics rule specification file 274 | dotnet_diagnostic.CA1509.severity = error 275 | 276 | # CA1700: Do not name enum values 'Reserved' 277 | dotnet_diagnostic.CA1700.severity = error 278 | 279 | # CA1707: Identifiers should not contain underscores 280 | dotnet_diagnostic.CA1707.severity = error 281 | 282 | # CA1708: Identifiers should differ by more than case 283 | dotnet_diagnostic.CA1708.severity = error 284 | 285 | # CA1710: Identifiers should have correct suffix 286 | dotnet_diagnostic.CA1710.severity = error 287 | 288 | # CA1711: Identifiers should not have incorrect suffix 289 | dotnet_diagnostic.CA1711.severity = error 290 | 291 | # CA1712: Do not prefix enum values with type name 292 | dotnet_diagnostic.CA1712.severity = error 293 | 294 | # CA1713: Events should not have 'Before' or 'After' prefix 295 | dotnet_diagnostic.CA1713.severity = error 296 | 297 | # CA1715: Identifiers should have correct prefix 298 | dotnet_diagnostic.CA1715.severity = error 299 | 300 | # CA1716: Identifiers should not match keywords 301 | dotnet_diagnostic.CA1716.severity = error 302 | 303 | # CA1720: Identifier contains type name 304 | dotnet_diagnostic.CA1720.severity = error 305 | 306 | # CA1721: Property names should not match get methods 307 | dotnet_diagnostic.CA1721.severity = error 308 | 309 | # CA1724: Type names should not match namespaces 310 | dotnet_diagnostic.CA1724.severity = error 311 | 312 | # CA1725: Parameter names should match base declaration 313 | dotnet_diagnostic.CA1725.severity = error 314 | 315 | # CA1801: Review unused parameters 316 | dotnet_diagnostic.CA1801.severity = error 317 | 318 | # CA1802: Use literals where appropriate 319 | dotnet_diagnostic.CA1802.severity = error 320 | 321 | # CA1805: Do not initialize unnecessarily 322 | dotnet_diagnostic.CA1805.severity = error 323 | 324 | # CA1806: Do not ignore method results 325 | dotnet_diagnostic.CA1806.severity = error 326 | 327 | # CA1810: Initialize reference type static fields inline 328 | dotnet_diagnostic.CA1810.severity = error 329 | 330 | # CA1812: Avoid uninstantiated internal classes 331 | dotnet_diagnostic.CA1812.severity = error 332 | 333 | # CA1813: Avoid unsealed attributes 334 | dotnet_diagnostic.CA1813.severity = error 335 | 336 | # CA1814: Prefer jagged arrays over multidimensional 337 | dotnet_diagnostic.CA1814.severity = error 338 | 339 | # CA1819: Properties should not return arrays 340 | dotnet_diagnostic.CA1819.severity = error 341 | 342 | # CA1820: Test for empty strings using string length 343 | dotnet_diagnostic.CA1820.severity = error 344 | 345 | # CA1821: Remove empty Finalizers 346 | dotnet_diagnostic.CA1821.severity = error 347 | 348 | # CA1822: Mark members as static 349 | dotnet_diagnostic.CA1822.severity = error 350 | 351 | # CA1823: Avoid unused private fields 352 | dotnet_diagnostic.CA1823.severity = error 353 | 354 | # CA1824: Mark assemblies with NeutralResourcesLanguageAttribute 355 | dotnet_diagnostic.CA1824.severity = error 356 | 357 | # CA1826: Do not use Enumerable methods on indexable collections 358 | dotnet_diagnostic.CA1826.severity = error 359 | 360 | # CA1827: Do not use Count() or LongCount() when Any() can be used 361 | dotnet_diagnostic.CA1827.severity = error 362 | 363 | # CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used 364 | dotnet_diagnostic.CA1828.severity = error 365 | 366 | # CA1829: Use Length/Count property instead of Count() when available 367 | dotnet_diagnostic.CA1829.severity = error 368 | 369 | # CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder 370 | dotnet_diagnostic.CA1830.severity = error 371 | 372 | # CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate 373 | dotnet_diagnostic.CA1831.severity = error 374 | 375 | # CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate 376 | dotnet_diagnostic.CA1832.severity = error 377 | 378 | # CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate 379 | dotnet_diagnostic.CA1833.severity = error 380 | 381 | # CA1834: Consider using 'StringBuilder.Append(char)' when applicable 382 | dotnet_diagnostic.CA1834.severity = error 383 | 384 | # CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' 385 | dotnet_diagnostic.CA1835.severity = error 386 | 387 | # CA1836: Prefer IsEmpty over Count 388 | dotnet_diagnostic.CA1836.severity = error 389 | 390 | # CA1837: Use 'Environment.ProcessId' 391 | dotnet_diagnostic.CA1837.severity = error 392 | 393 | # CA1838: Avoid 'StringBuilder' parameters for P/Invokes 394 | dotnet_diagnostic.CA1838.severity = error 395 | 396 | # CA2000: Dispose objects before losing scope 397 | dotnet_diagnostic.CA2000.severity = error 398 | 399 | # CA2002: Do not lock on objects with weak identity 400 | dotnet_diagnostic.CA2002.severity = error 401 | 402 | # CA2007: Consider calling ConfigureAwait on the awaited task 403 | dotnet_diagnostic.CA2007.severity = error 404 | 405 | # CA2008: Do not create tasks without passing a TaskScheduler 406 | dotnet_diagnostic.CA2008.severity = error 407 | 408 | # CA2009: Do not call ToImmutableCollection on an ImmutableCollection value 409 | dotnet_diagnostic.CA2009.severity = error 410 | 411 | # CA2011: Avoid infinite recursion 412 | dotnet_diagnostic.CA2011.severity = error 413 | 414 | # CA2012: Use ValueTasks correctly 415 | dotnet_diagnostic.CA2012.severity = error 416 | 417 | # CA2013: Do not use ReferenceEquals with value types 418 | dotnet_diagnostic.CA2013.severity = error 419 | 420 | # CA2014: Do not use stackalloc in loops 421 | dotnet_diagnostic.CA2014.severity = error 422 | 423 | # CA2015: Do not define finalizers for types derived from MemoryManager 424 | dotnet_diagnostic.CA2015.severity = error 425 | 426 | # CA2016: Forward the 'CancellationToken' parameter to methods that take one 427 | dotnet_diagnostic.CA2016.severity = error 428 | 429 | # CA2100: Review SQL queries for security vulnerabilities 430 | dotnet_diagnostic.CA2100.severity = error 431 | 432 | # CA2101: Specify marshaling for P/Invoke string arguments 433 | dotnet_diagnostic.CA2101.severity = error 434 | 435 | # CA2109: Review visible event handlers 436 | dotnet_diagnostic.CA2109.severity = error 437 | 438 | # CA2119: Seal methods that satisfy private interfaces 439 | dotnet_diagnostic.CA2119.severity = error 440 | 441 | # CA2153: Do Not Catch Corrupted State Exceptions 442 | dotnet_diagnostic.CA2153.severity = error 443 | 444 | # CA2200: Rethrow to preserve stack details 445 | dotnet_diagnostic.CA2200.severity = error 446 | 447 | # CA2201: Do not raise reserved exception types 448 | dotnet_diagnostic.CA2201.severity = error 449 | 450 | # CA2207: Initialize value type static fields inline 451 | dotnet_diagnostic.CA2207.severity = error 452 | 453 | # CA2208: Instantiate argument exceptions correctly 454 | dotnet_diagnostic.CA2208.severity = error 455 | 456 | # CA2211: Non-constant fields should not be visible 457 | dotnet_diagnostic.CA2211.severity = error 458 | 459 | # CA2213: Disposable fields should be disposed 460 | dotnet_diagnostic.CA2213.severity = error 461 | 462 | # CA2214: Do not call overridable methods in constructors 463 | dotnet_diagnostic.CA2214.severity = error 464 | 465 | # CA2215: Dispose methods should call base class dispose 466 | dotnet_diagnostic.CA2215.severity = error 467 | 468 | # CA2216: Disposable types should declare finalizer 469 | dotnet_diagnostic.CA2216.severity = error 470 | 471 | # CA2217: Do not mark enums with FlagsAttribute 472 | dotnet_diagnostic.CA2217.severity = error 473 | 474 | # CA2218: Override GetHashCode on overriding Equals 475 | dotnet_diagnostic.CA2218.severity = error 476 | 477 | # CA2219: Do not raise exceptions in finally clauses 478 | dotnet_diagnostic.CA2219.severity = error 479 | 480 | # CA2224: Override Equals on overloading operator equals 481 | dotnet_diagnostic.CA2224.severity = error 482 | 483 | # CA2225: Operator overloads have named alternates 484 | dotnet_diagnostic.CA2225.severity = error 485 | 486 | # CA2226: Operators should have symmetrical overloads 487 | dotnet_diagnostic.CA2226.severity = error 488 | 489 | # CA2227: Collection properties should be read only 490 | dotnet_diagnostic.CA2227.severity = error 491 | 492 | # CA2229: Implement serialization constructors 493 | dotnet_diagnostic.CA2229.severity = error 494 | 495 | # CA2231: Overload operator equals on overriding value type Equals 496 | dotnet_diagnostic.CA2231.severity = error 497 | 498 | # CA2234: Pass system uri objects instead of strings 499 | dotnet_diagnostic.CA2234.severity = error 500 | 501 | # CA2235: Mark all non-serializable fields 502 | dotnet_diagnostic.CA2235.severity = error 503 | 504 | # CA2241: Provide correct arguments to formatting methods 505 | dotnet_diagnostic.CA2241.severity = error 506 | 507 | # CA2242: Test for NaN correctly 508 | dotnet_diagnostic.CA2242.severity = error 509 | 510 | # CA2243: Attribute string literals should parse correctly 511 | dotnet_diagnostic.CA2243.severity = error 512 | 513 | # CA2244: Do not duplicate indexed element initializations 514 | dotnet_diagnostic.CA2244.severity = error 515 | 516 | # CA2245: Do not assign a property to itself 517 | dotnet_diagnostic.CA2245.severity = error 518 | 519 | # CA2246: Assigning symbol and its member in the same statement 520 | dotnet_diagnostic.CA2246.severity = error 521 | 522 | # CA2247: Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum 523 | dotnet_diagnostic.CA2247.severity = error 524 | 525 | # CA2248: Provide correct 'enum' argument to 'Enum.HasFlag' 526 | dotnet_diagnostic.CA2248.severity = error 527 | 528 | # CA2249: Consider using 'string.Contains' instead of 'string.IndexOf' 529 | dotnet_diagnostic.CA2249.severity = error 530 | 531 | # CA2300: Do not use insecure deserializer BinaryFormatter 532 | dotnet_diagnostic.CA2300.severity = error 533 | 534 | # CA2301: Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder 535 | dotnet_diagnostic.CA2301.severity = error 536 | 537 | # CA2302: Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize 538 | dotnet_diagnostic.CA2302.severity = error 539 | 540 | # CA2305: Do not use insecure deserializer LosFormatter 541 | dotnet_diagnostic.CA2305.severity = error 542 | 543 | # CA2310: Do not use insecure deserializer NetDataContractSerializer 544 | dotnet_diagnostic.CA2310.severity = error 545 | 546 | # CA2311: Do not deserialize without first setting NetDataContractSerializer.Binder 547 | dotnet_diagnostic.CA2311.severity = error 548 | 549 | # CA2312: Ensure NetDataContractSerializer.Binder is set before deserializing 550 | dotnet_diagnostic.CA2312.severity = error 551 | 552 | # CA2315: Do not use insecure deserializer ObjectStateFormatter 553 | dotnet_diagnostic.CA2315.severity = error 554 | 555 | # CA2321: Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver 556 | dotnet_diagnostic.CA2321.severity = error 557 | 558 | # CA2322: Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing 559 | dotnet_diagnostic.CA2322.severity = error 560 | 561 | # CA2326: Do not use TypeNameHandling values other than None 562 | dotnet_diagnostic.CA2326.severity = error 563 | 564 | # CA2327: Do not use insecure JsonSerializerSettings 565 | dotnet_diagnostic.CA2327.severity = error 566 | 567 | # CA2328: Ensure that JsonSerializerSettings are secure 568 | dotnet_diagnostic.CA2328.severity = error 569 | 570 | # CA2329: Do not deserialize with JsonSerializer using an insecure configuration 571 | dotnet_diagnostic.CA2329.severity = error 572 | 573 | # CA2330: Ensure that JsonSerializer has a secure configuration when deserializing 574 | dotnet_diagnostic.CA2330.severity = error 575 | 576 | # CA2350: Do not use DataTable.ReadXml() with untrusted data 577 | dotnet_diagnostic.CA2350.severity = error 578 | 579 | # CA2351: Do not use DataSet.ReadXml() with untrusted data 580 | dotnet_diagnostic.CA2351.severity = error 581 | 582 | # CA2352: Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks 583 | dotnet_diagnostic.CA2352.severity = error 584 | 585 | # CA2353: Unsafe DataSet or DataTable in serializable type 586 | dotnet_diagnostic.CA2353.severity = error 587 | 588 | # CA2354: Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks 589 | dotnet_diagnostic.CA2354.severity = error 590 | 591 | # CA2355: Unsafe DataSet or DataTable type found in deserializable object graph 592 | dotnet_diagnostic.CA2355.severity = error 593 | 594 | # CA2356: Unsafe DataSet or DataTable type in web deserializable object graph 595 | dotnet_diagnostic.CA2356.severity = error 596 | 597 | # CA2361: Ensure auto-generated class containing DataSet.ReadXml() is not used with untrusted data 598 | dotnet_diagnostic.CA2361.severity = error 599 | 600 | # CA2362: Unsafe DataSet or DataTable in auto-generated serializable type can be vulnerable to remote code execution attacks 601 | dotnet_diagnostic.CA2362.severity = error 602 | 603 | # CA3001: Review code for SQL injection vulnerabilities 604 | dotnet_diagnostic.CA3001.severity = error 605 | 606 | # CA3002: Review code for XSS vulnerabilities 607 | dotnet_diagnostic.CA3002.severity = error 608 | 609 | # CA3003: Review code for file path injection vulnerabilities 610 | dotnet_diagnostic.CA3003.severity = error 611 | 612 | # CA3004: Review code for information disclosure vulnerabilities 613 | dotnet_diagnostic.CA3004.severity = error 614 | 615 | # CA3005: Review code for LDAP injection vulnerabilities 616 | dotnet_diagnostic.CA3005.severity = error 617 | 618 | # CA3006: Review code for process command injection vulnerabilities 619 | dotnet_diagnostic.CA3006.severity = error 620 | 621 | # CA3007: Review code for open redirect vulnerabilities 622 | dotnet_diagnostic.CA3007.severity = error 623 | 624 | # CA3008: Review code for XPath injection vulnerabilities 625 | dotnet_diagnostic.CA3008.severity = error 626 | 627 | # CA3009: Review code for XML injection vulnerabilities 628 | dotnet_diagnostic.CA3009.severity = error 629 | 630 | # CA3010: Review code for XAML injection vulnerabilities 631 | dotnet_diagnostic.CA3010.severity = error 632 | 633 | # CA3011: Review code for DLL injection vulnerabilities 634 | dotnet_diagnostic.CA3011.severity = error 635 | 636 | # CA3012: Review code for regex injection vulnerabilities 637 | dotnet_diagnostic.CA3012.severity = error 638 | 639 | # CA3061: Do Not Add Schema By URL 640 | dotnet_diagnostic.CA3061.severity = error 641 | 642 | # CA3075: Insecure DTD processing in XML 643 | dotnet_diagnostic.CA3075.severity = error 644 | 645 | # CA3076: Insecure XSLT script processing. 646 | dotnet_diagnostic.CA3076.severity = error 647 | 648 | # CA3077: Insecure Processing in API Design, XmlDocument and XmlTextReader 649 | dotnet_diagnostic.CA3077.severity = error 650 | 651 | # CA3147: Mark Verb Handlers With Validate Antiforgery Token 652 | dotnet_diagnostic.CA3147.severity = error 653 | 654 | # CA5350: Do Not Use Weak Cryptographic Algorithms 655 | dotnet_diagnostic.CA5350.severity = error 656 | 657 | # CA5351: Do Not Use Broken Cryptographic Algorithms 658 | dotnet_diagnostic.CA5351.severity = error 659 | 660 | # CA5358: Review cipher mode usage with cryptography experts 661 | dotnet_diagnostic.CA5358.severity = error 662 | 663 | # CA5359: Do Not Disable Certificate Validation 664 | dotnet_diagnostic.CA5359.severity = error 665 | 666 | # CA5360: Do Not Call Dangerous Methods In Deserialization 667 | dotnet_diagnostic.CA5360.severity = error 668 | 669 | # CA5361: Do Not Disable SChannel Use of Strong Crypto 670 | dotnet_diagnostic.CA5361.severity = error 671 | 672 | # CA5362: Potential reference cycle in deserialized object graph 673 | dotnet_diagnostic.CA5362.severity = error 674 | 675 | # CA5363: Do Not Disable Request Validation 676 | dotnet_diagnostic.CA5363.severity = error 677 | 678 | # CA5364: Do Not Use Deprecated Security Protocols 679 | dotnet_diagnostic.CA5364.severity = error 680 | 681 | # CA5365: Do Not Disable HTTP Header Checking 682 | dotnet_diagnostic.CA5365.severity = error 683 | 684 | # CA5366: Use XmlReader for 'DataSet.ReadXml()' 685 | dotnet_diagnostic.CA5366.severity = error 686 | 687 | # CA5367: Do Not Serialize Types With Pointer Fields 688 | dotnet_diagnostic.CA5367.severity = error 689 | 690 | # CA5368: Set ViewStateUserKey For Classes Derived From Page 691 | dotnet_diagnostic.CA5368.severity = error 692 | 693 | # CA5369: Use XmlReader for 'XmlSerializer.Deserialize()' 694 | dotnet_diagnostic.CA5369.severity = error 695 | 696 | # CA5370: Use XmlReader for XmlValidatingReader constructor 697 | dotnet_diagnostic.CA5370.severity = error 698 | 699 | # CA5371: Use XmlReader for 'XmlSchema.Read()' 700 | dotnet_diagnostic.CA5371.severity = error 701 | 702 | # CA5372: Use XmlReader for XPathDocument constructor 703 | dotnet_diagnostic.CA5372.severity = error 704 | 705 | # CA5373: Do not use obsolete key derivation function 706 | dotnet_diagnostic.CA5373.severity = error 707 | 708 | # CA5374: Do Not Use XslTransform 709 | dotnet_diagnostic.CA5374.severity = error 710 | 711 | # CA5375: Do Not Use Account Shared Access Signature 712 | dotnet_diagnostic.CA5375.severity = error 713 | 714 | # CA5376: Use SharedAccessProtocol HttpsOnly 715 | dotnet_diagnostic.CA5376.severity = error 716 | 717 | # CA5377: Use Container Level Access Policy 718 | dotnet_diagnostic.CA5377.severity = error 719 | 720 | # CA5378: Do not disable ServicePointManagerSecurityProtocols 721 | dotnet_diagnostic.CA5378.severity = error 722 | 723 | # CA5379: Ensure Key Derivation Function algorithm is sufficiently strong 724 | dotnet_diagnostic.CA5379.severity = error 725 | 726 | # CA5380: Do Not Add Certificates To Root Store 727 | dotnet_diagnostic.CA5380.severity = error 728 | 729 | # CA5381: Ensure Certificates Are Not Added To Root Store 730 | dotnet_diagnostic.CA5381.severity = error 731 | 732 | # CA5382: Use Secure Cookies In ASP.Net Core 733 | dotnet_diagnostic.CA5382.severity = error 734 | 735 | # CA5383: Ensure Use Secure Cookies In ASP.Net Core 736 | dotnet_diagnostic.CA5383.severity = error 737 | 738 | # CA5384: Do Not Use Digital Signature Algorithm (DSA) 739 | dotnet_diagnostic.CA5384.severity = error 740 | 741 | # CA5385: Use Rivest–Shamir–Adleman (RSA) Algorithm With Sufficient Key Size 742 | dotnet_diagnostic.CA5385.severity = error 743 | 744 | # CA5386: Avoid hardcoding SecurityProtocolType value 745 | dotnet_diagnostic.CA5386.severity = error 746 | 747 | # CA5387: Do Not Use Weak Key Derivation Function With Insufficient Iteration Count 748 | dotnet_diagnostic.CA5387.severity = error 749 | 750 | # CA5388: Ensure Sufficient Iteration Count When Using Weak Key Derivation Function 751 | dotnet_diagnostic.CA5388.severity = error 752 | 753 | # CA5389: Do Not Add Archive Item's Path To The Target File System Path 754 | dotnet_diagnostic.CA5389.severity = error 755 | 756 | # CA5390: Do not hard-code encryption key 757 | dotnet_diagnostic.CA5390.severity = error 758 | 759 | # CA5391: Use antiforgery tokens in ASP.NET Core MVC controllers 760 | dotnet_diagnostic.CA5391.severity = error 761 | 762 | # CA5392: Use DefaultDllImportSearchPaths attribute for P/Invokes 763 | dotnet_diagnostic.CA5392.severity = error 764 | 765 | # CA5393: Do not use unsafe DllImportSearchPath value 766 | dotnet_diagnostic.CA5393.severity = error 767 | 768 | # CA5394: Do not use insecure randomness 769 | dotnet_diagnostic.CA5394.severity = error 770 | 771 | # CA5395: Miss HttpVerb attribute for action methods 772 | dotnet_diagnostic.CA5395.severity = error 773 | 774 | # CA5396: Set HttpOnly to true for HttpCookie 775 | dotnet_diagnostic.CA5396.severity = error 776 | 777 | # CA5397: Do not use deprecated SslProtocols values 778 | dotnet_diagnostic.CA5397.severity = error 779 | 780 | # CA5398: Avoid hardcoded SslProtocols values 781 | dotnet_diagnostic.CA5398.severity = error 782 | 783 | # CA5399: HttpClients should enable certificate revocation list checks 784 | dotnet_diagnostic.CA5399.severity = error 785 | 786 | # CA5400: Ensure HttpClient certificate revocation list check is not disabled 787 | dotnet_diagnostic.CA5400.severity = error 788 | 789 | # CA5401: Do not use CreateEncryptor with non-default IV 790 | dotnet_diagnostic.CA5401.severity = error 791 | 792 | # CA5402: Use CreateEncryptor with the default IV 793 | dotnet_diagnostic.CA5402.severity = error 794 | 795 | # CA5403: Do not hard-code certificate 796 | dotnet_diagnostic.CA5403.severity = error 797 | 798 | # IL3000: Avoid using accessing Assembly file path when publishing as a single-file 799 | dotnet_diagnostic.IL3000.severity = error 800 | 801 | # IL3001: Avoid using accessing Assembly file path when publishing as a single-file 802 | dotnet_diagnostic.IL3001.severity = error 803 | 804 | # CA1815: Override equals and operator equals on value types 805 | dotnet_diagnostic.CA1815.severity = error 806 | 807 | # CA1816: Dispose methods should call SuppressFinalize 808 | dotnet_diagnostic.CA1816.severity = error 809 | 810 | # CA1063: Implement IDisposable Correctly 811 | dotnet_diagnostic.CA1063.severity = error 812 | 813 | dotnet_diagnostic.CA2237.severity = None 814 | 815 | dotnet_diagnostic.CA1014.severity = None 816 | 817 | #Why doesn't this setting work? 818 | #csharp_style_expression_bodied_local_functions = true; 819 | dotnet_diagnostic.IDE0061.severity = none 820 | 821 | dotnet_diagnostic.IDE0130.severity = none 822 | 823 | # CA1062: Validate arguments of public methods 824 | dotnet_diagnostic.CA1062.severity = none 825 | 826 | # IDE0160: Convert to block scoped namespace 827 | csharp_style_namespace_declarations = file_scoped 828 | --------------------------------------------------------------------------------