diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index ef7d0fcc..bdc96713 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -99,7 +99,7 @@ jobs:
- name: Restore
run: dotnet restore "${{ env.PROJECT_PATH }}"
- - name: Build
+ - name: 🔨 Build
run: >-
dotnet build "${{ env.PROJECT_PATH }}"
--configuration "${{ env.CONFIGURATION }}"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index f3c32f01..a20b2882 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -128,7 +128,7 @@ jobs:
- name: Install dependencies
run: dotnet restore "${{ env.PROJECT_PATH }}"
- - name: Create the package
+ - name: 📦 Create the package
run: >-
dotnet pack "${{ env.PROJECT_PATH }}"
--output ./artifacts
@@ -140,7 +140,7 @@ jobs:
-p:IncludeSymbols=true
-p:SymbolPackageFormat=snupkg
- - name: Create the package - Signed
+ - name: 📦 Create the package - Signed
run: >-
dotnet pack "${{ env.PROJECT_PATH }}"
--output ./artifacts
diff --git a/Directory.Build.props b/Directory.Build.props
index 3d16a055..f2b9a699 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -5,5 +5,17 @@
strict
true
+
+
+ true
+ true
+ true
+ true
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 00000000..d5faa965
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,29 @@
+
+
+ |net20|net40|net45|net451|net452|net46|
+ |net20|net40|net45|net451|net452|net46|net461|
+ |net45|net451|net452|net46|
+ |net45|net451|net452|net46|net461|
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 5a788f19..4cb6caf7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,8 +4,8 @@ services:
redmine:
ports:
- '8089:3000'
- image: 'redmine:5.1.1-alpine'
- container_name: 'redmine-web'
+ image: 'redmine:6.0.5-alpine'
+ container_name: 'redmine-web605'
depends_on:
- db-postgres
# healthcheck:
@@ -32,8 +32,8 @@ services:
POSTGRES_DB: redmine
POSTGRES_USER: redmine-usr
POSTGRES_PASSWORD: redmine-pswd
- container_name: 'redmine-db'
- image: 'postgres:16-alpine'
+ container_name: 'redmine-db175'
+ image: 'postgres:17.5-alpine'
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 20s
diff --git a/global.json b/global.json
index 6223f1b6..1f044567 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "9.0.101",
+ "version": "9.0.203",
"allowPrerelease": false,
"rollForward": "latestMajor"
}
diff --git a/redmine-net-api.sln b/redmine-net-api.sln
index 86cc3bbe..6e9f665f 100644
--- a/redmine-net-api.sln
+++ b/redmine-net-api.sln
@@ -38,6 +38,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{707B6A3F
releasenotes.props = releasenotes.props
signing.props = signing.props
version.props = version.props
+ Directory.Packages.props = Directory.Packages.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{1D340EEB-C535-45D4-80D7-ADD4434D7B77}"
diff --git a/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs
index a037a60c..c1d59744 100644
--- a/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs
+++ b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs
@@ -15,6 +15,7 @@ limitations under the License.
*/
using System.Net;
+using Redmine.Net.Api.Extensions;
namespace Redmine.Net.Api.Authentication;
@@ -24,7 +25,7 @@ namespace Redmine.Net.Api.Authentication;
public sealed class RedmineApiKeyAuthentication: IRedmineAuthentication
{
///
- public string AuthenticationType => "X-Redmine-API-Key";
+ public string AuthenticationType { get; } = RedmineAuthenticationType.ApiKey.ToText();
///
public string Token { get; init; }
diff --git a/src/redmine-net-api/Authentication/RedmineAuthenticationType.cs b/src/redmine-net-api/Authentication/RedmineAuthenticationType.cs
new file mode 100644
index 00000000..22c38cd2
--- /dev/null
+++ b/src/redmine-net-api/Authentication/RedmineAuthenticationType.cs
@@ -0,0 +1,8 @@
+namespace Redmine.Net.Api.Authentication;
+
+internal enum RedmineAuthenticationType
+{
+ NoAuthentication,
+ Basic,
+ ApiKey
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs
index e78aa653..810da00a 100644
--- a/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs
+++ b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs
@@ -18,6 +18,7 @@ limitations under the License.
using System.Net;
using System.Text;
using Redmine.Net.Api.Exceptions;
+using Redmine.Net.Api.Extensions;
namespace Redmine.Net.Api.Authentication
{
@@ -27,7 +28,7 @@ namespace Redmine.Net.Api.Authentication
public sealed class RedmineBasicAuthentication: IRedmineAuthentication
{
///
- public string AuthenticationType => "Basic";
+ public string AuthenticationType { get; } = RedmineAuthenticationType.Basic.ToText();
///
public string Token { get; init; }
@@ -45,7 +46,7 @@ public RedmineBasicAuthentication(string username, string password)
if (username == null) throw new RedmineException(nameof(username));
if (password == null) throw new RedmineException(nameof(password));
- Token = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
+ Token = $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"))}";
}
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs
index 4f2ed673..d8518828 100644
--- a/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs
+++ b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs
@@ -15,6 +15,7 @@ limitations under the License.
*/
using System.Net;
+using Redmine.Net.Api.Extensions;
namespace Redmine.Net.Api.Authentication;
@@ -24,7 +25,7 @@ namespace Redmine.Net.Api.Authentication;
public sealed class RedmineNoAuthentication: IRedmineAuthentication
{
///
- public string AuthenticationType => "NoAuth";
+ public string AuthenticationType { get; } = RedmineAuthenticationType.NoAuthentication.ToText();
///
public string Token { get; init; }
diff --git a/src/redmine-net-api/Common/AType.cs b/src/redmine-net-api/Common/AType.cs
new file mode 100644
index 00000000..80d0493c
--- /dev/null
+++ b/src/redmine-net-api/Common/AType.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Redmine.Net.Api.Common;
+
+internal readonly struct A{
+ public static A Is => default;
+#pragma warning disable CS0184 // 'is' expression's given expression is never of the provided type
+ public static bool IsEqual() => Is is A;
+#pragma warning restore CS0184 // 'is' expression's given expression is never of the provided type
+ public static Type Value => typeof(T);
+
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Common/ArgumentVerifier.cs b/src/redmine-net-api/Common/ArgumentVerifier.cs
new file mode 100644
index 00000000..c4e7ede7
--- /dev/null
+++ b/src/redmine-net-api/Common/ArgumentVerifier.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Redmine.Net.Api.Common;
+
+///
+/// A utility class to perform argument validations.
+///
+internal static class ArgumentVerifier
+{
+ ///
+ /// Throws ArgumentNullException if the argument is null.
+ ///
+ /// Argument value to check.
+ /// Name of Argument.
+ public static void ThrowIfNull([NotNull] object value, string name)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(name);
+ }
+ }
+
+ ///
+ /// Validates string and throws:
+ /// ArgumentNullException if the argument is null.
+ /// ArgumentException if the argument is empty.
+ ///
+ /// Argument value to check.
+ /// Name of Argument.
+ public static void ThrowIfNullOrEmpty([NotNull] string value, string name)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(name);
+ }
+
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new ArgumentException("The value cannot be null or empty", name);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Types/IValue.cs b/src/redmine-net-api/Common/IValue.cs
similarity index 95%
rename from src/redmine-net-api/Types/IValue.cs
rename to src/redmine-net-api/Common/IValue.cs
index bbfe3a77..d95d24eb 100755
--- a/src/redmine-net-api/Types/IValue.cs
+++ b/src/redmine-net-api/Common/IValue.cs
@@ -14,7 +14,7 @@ You may obtain a copy of the License at
limitations under the License.
*/
-namespace Redmine.Net.Api.Types
+namespace Redmine.Net.Api.Common
{
///
///
diff --git a/src/redmine-net-api/Types/PagedResults.cs b/src/redmine-net-api/Common/PagedResults.cs
similarity index 91%
rename from src/redmine-net-api/Types/PagedResults.cs
rename to src/redmine-net-api/Common/PagedResults.cs
index eab0c680..af1e82fd 100644
--- a/src/redmine-net-api/Types/PagedResults.cs
+++ b/src/redmine-net-api/Common/PagedResults.cs
@@ -16,7 +16,7 @@ limitations under the License.
using System.Collections.Generic;
-namespace Redmine.Net.Api.Serialization
+namespace Redmine.Net.Api.Common
{
///
///
@@ -30,7 +30,7 @@ public sealed class PagedResults where TOut: class
///
///
///
- public PagedResults(IEnumerable items, int total, int offset, int pageSize)
+ public PagedResults(List items, int total, int offset, int pageSize)
{
Items = items;
TotalItems = total;
@@ -75,6 +75,6 @@ public PagedResults(IEnumerable items, int total, int offset, int pageSize
///
///
///
- public IEnumerable Items { get; }
+ public List Items { get; }
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Exceptions/RedmineException.cs b/src/redmine-net-api/Exceptions/RedmineException.cs
index db791454..ee2cc07a 100644
--- a/src/redmine-net-api/Exceptions/RedmineException.cs
+++ b/src/redmine-net-api/Exceptions/RedmineException.cs
@@ -15,6 +15,7 @@ limitations under the License.
*/
using System;
+using System.Diagnostics;
using System.Globalization;
using System.Runtime.Serialization;
@@ -24,6 +25,7 @@ namespace Redmine.Net.Api.Exceptions
/// Thrown in case something went wrong in Redmine
///
///
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[Serializable]
public class RedmineException : Exception
{
@@ -85,5 +87,7 @@ protected RedmineException(SerializationInfo serializationInfo, StreamingContext
}
#endif
+
+ private string DebuggerDisplay => $"[{Message}]";
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Exceptions/RedmineSerializationException.cs b/src/redmine-net-api/Exceptions/RedmineSerializationException.cs
new file mode 100644
index 00000000..0f6b779f
--- /dev/null
+++ b/src/redmine-net-api/Exceptions/RedmineSerializationException.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace Redmine.Net.Api.Exceptions;
+
+///
+/// Represents an error that occurs during JSON serialization or deserialization.
+///
+public class RedmineSerializationException : RedmineException
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RedmineSerializationException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ /// The error message that explains the reason for the exception.
+ public RedmineSerializationException(string message) : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// /// The name of the parameter that caused the exception.
+ public RedmineSerializationException(string message, string paramName) : base(message)
+ {
+ ParamName = paramName;
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified.
+ public RedmineSerializationException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Gets the name of the parameter that caused the current exception.
+ ///
+ public string ParamName { get; }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Extensions/CollectionExtensions.cs b/src/redmine-net-api/Extensions/CollectionExtensions.cs
deleted file mode 100755
index 6b56e79c..00000000
--- a/src/redmine-net-api/Extensions/CollectionExtensions.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Collections.Generic;
-using System.Text;
-using Redmine.Net.Api.Types;
-
-namespace Redmine.Net.Api.Extensions
-{
- ///
- ///
- ///
- public static class CollectionExtensions
- {
- ///
- /// Clones the specified list to clone.
- ///
- ///
- /// The list to clone.
- ///
- ///
- public static IList Clone(this IList listToClone, bool resetId) where T : ICloneable
- {
- if (listToClone == null)
- {
- return null;
- }
-
- var clonedList = new List();
-
- for (var index = 0; index < listToClone.Count; index++)
- {
- var item = listToClone[index];
- clonedList.Add(item.Clone(resetId));
- }
-
- return clonedList;
- }
-
- ///
- ///
- ///
- ///
- /// The list.
- /// The list to compare.
- ///
- public static bool Equals(this IList list, IList listToCompare) where T : class
- {
- if (list == null || listToCompare == null)
- {
- return false;
- }
-
- if (list.Count != listToCompare.Count)
- {
- return false;
- }
-
- var index = 0;
- while (index < list.Count && list[index].Equals(listToCompare[index]))
- {
- index++;
- }
-
- return index == list.Count;
- }
-
- ///
- ///
- ///
- ///
- public static string Dump(this IEnumerable collection) where TIn : class
- {
- if (collection == null)
- {
- return null;
- }
-
- var sb = new StringBuilder("{");
-
- foreach (var item in collection)
- {
- sb.Append(item).Append(',');
- }
-
- if (sb.Length > 1)
- {
- sb.Length -= 1;
- }
-
- sb.Append('}');
-
- var str = sb.ToString();
- sb.Length = 0;
-
- return str;
- }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Extensions/EnumExtensions.cs b/src/redmine-net-api/Extensions/EnumExtensions.cs
new file mode 100644
index 00000000..60b6499e
--- /dev/null
+++ b/src/redmine-net-api/Extensions/EnumExtensions.cs
@@ -0,0 +1,113 @@
+using System;
+using Redmine.Net.Api.Authentication;
+using Redmine.Net.Api.Types;
+
+namespace Redmine.Net.Api.Extensions;
+
+///
+/// Provides extension methods for enumerations used in the Redmine.Net.Api.Types namespace.
+///
+public static class EnumExtensions
+{
+ ///
+ /// Converts the specified enumeration value to its lowercase invariant string representation.
+ ///
+ /// The enumeration value to be converted.
+ /// A string representation of the IssueRelationType enumeration value in a lowercase, or "undefined" if the value does not match a defined case.
+ public static string ToLowerName(this IssueRelationType @enum)
+ {
+ return @enum switch
+ {
+ IssueRelationType.Relates => "relates",
+ IssueRelationType.Duplicates => "duplicates",
+ IssueRelationType.Duplicated => "duplicated",
+ IssueRelationType.Blocks => "blocks",
+ IssueRelationType.Blocked => "blocked",
+ IssueRelationType.Precedes => "precedes",
+ IssueRelationType.Follows => "follows",
+ IssueRelationType.CopiedTo => "copied_to",
+ IssueRelationType.CopiedFrom => "copied_from",
+ _ => "undefined"
+ };
+ }
+
+ ///
+ /// Converts the specified VersionSharing enumeration value to its lowercase invariant string representation.
+ ///
+ /// The VersionSharing enumeration value to be converted.
+ /// A string representation of the VersionSharing enumeration value in a lowercase, or "undefined" if the value does not match a valid case.
+ public static string ToLowerName(this VersionSharing @enum)
+ {
+ return @enum switch
+ {
+ VersionSharing.Unknown => "unknown",
+ VersionSharing.None => "none",
+ VersionSharing.Descendants => "descendants",
+ VersionSharing.Hierarchy => "hierarchy",
+ VersionSharing.Tree => "tree",
+ VersionSharing.System => "system",
+ _ => "undefined"
+ };
+ }
+
+ ///
+ /// Converts the specified enumeration value to its lowercase invariant string representation.
+ ///
+ /// The enumeration value to be converted.
+ /// A lowercase string representation of the enumeration value, or "undefined" if the value does not match a defined case.
+ public static string ToLowerName(this VersionStatus @enum)
+ {
+ return @enum switch
+ {
+ VersionStatus.None => "none",
+ VersionStatus.Open => "open",
+ VersionStatus.Closed => "closed",
+ VersionStatus.Locked => "locked",
+ _ => "undefined"
+ };
+ }
+
+ ///
+ /// Converts the specified ProjectStatus enumeration value to its lowercase invariant string representation.
+ ///
+ /// The ProjectStatus enumeration value to be converted.
+ /// A string representation of the ProjectStatus enumeration value in a lowercase, or "undefined" if the value does not match a defined case.
+ public static string ToLowerName(this ProjectStatus @enum)
+ {
+ return @enum switch
+ {
+ ProjectStatus.None => "none",
+ ProjectStatus.Active => "active",
+ ProjectStatus.Archived => "archived",
+ ProjectStatus.Closed => "closed",
+ _ => "undefined"
+ };
+ }
+
+ ///
+ /// Converts the specified enumeration value to its lowercase invariant string representation.
+ ///
+ /// The enumeration value to be converted.
+ /// A string representation of the UserStatus enumeration value in a lowercase, or "undefined" if the value does not match a defined case.
+ public static string ToLowerName(this UserStatus @enum)
+ {
+ return @enum switch
+ {
+ UserStatus.StatusActive => "status_active",
+ UserStatus.StatusLocked => "status_locked",
+ UserStatus.StatusRegistered => "status_registered",
+ _ => "undefined"
+ };
+ }
+
+ internal static string ToText(this RedmineAuthenticationType @enum)
+ {
+ return @enum switch
+ {
+ RedmineAuthenticationType.NoAuthentication => "NoAuth",
+ RedmineAuthenticationType.Basic => "Basic",
+ RedmineAuthenticationType.ApiKey => "ApiKey",
+ _ => "undefined"
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Extensions/IEnumerableExtensions.cs b/src/redmine-net-api/Extensions/IEnumerableExtensions.cs
new file mode 100644
index 00000000..98d9b355
--- /dev/null
+++ b/src/redmine-net-api/Extensions/IEnumerableExtensions.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Redmine.Net.Api.Common;
+
+namespace Redmine.Net.Api.Extensions;
+
+///
+/// Provides extension methods for IEnumerable types.
+///
+public static class IEnumerableExtensions
+{
+ ///
+ /// Converts a collection of objects into a string representation with each item separated by a comma
+ /// and enclosed within curly braces.
+ ///
+ /// The type of items in the collection. The type must be a reference type.
+ /// The collection of items to convert to a string representation.
+ ///
+ /// Returns a string containing all the items from the collection, separated by commas and
+ /// enclosed within curly braces. Returns null if the collection is null.
+ ///
+ internal static string Dump(this IEnumerable collection) where TIn : class
+ {
+ if (collection == null)
+ {
+ return null;
+ }
+
+ var sb = new StringBuilder("{");
+
+ foreach (var item in collection)
+ {
+ sb.Append(item).Append(',');
+ }
+
+ if (sb.Length > 1)
+ {
+ sb.Length -= 1;
+ }
+
+ sb.Append('}');
+
+ var str = sb.ToString();
+ sb.Length = 0;
+
+ return str;
+ }
+
+ ///
+ /// Returns the index of the first item in the sequence that satisfies the predicate. If no item satisfies the predicate, -1 is returned.
+ ///
+ /// The type of objects in the .
+ /// in which to search.
+ /// Function performed to check whether an item satisfies the condition.
+ /// Return the zero-based index of the first occurrence of an element that satisfies the condition, if found; otherwise, -1.
+ internal static int IndexOf(this IEnumerable source, Func predicate)
+ {
+ ArgumentVerifier.ThrowIfNull(predicate, nameof(predicate));
+
+ var index = 0;
+
+ foreach (var item in source)
+ {
+ if (predicate(item))
+ {
+ return index;
+ }
+
+ index++;
+ }
+
+ return -1;
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs b/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs
new file mode 100644
index 00000000..beed972a
--- /dev/null
+++ b/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs
@@ -0,0 +1,82 @@
+using Redmine.Net.Api.Exceptions;
+using Redmine.Net.Api.Types;
+
+namespace Redmine.Net.Api.Extensions
+{
+ ///
+ ///
+ ///
+ public static class IdentifiableNameExtensions
+ {
+ ///
+ /// Converts an object of type into an object.
+ ///
+ /// The type of the entity to convert. Expected to be one of the supported Redmine entity types.
+ /// The entity object to be converted into an .
+ /// An object populated with the identifier and name of the specified entity, or null if the entity type is not supported.
+ public static IdentifiableName ToIdentifiableName(this T entity) where T : class
+ {
+ return entity switch
+ {
+ CustomField customField => IdentifiableName.Create(customField.Id, customField.Name),
+ CustomFieldRole customFieldRole => IdentifiableName.Create(customFieldRole.Id, customFieldRole.Name),
+ DocumentCategory documentCategory => IdentifiableName.Create(documentCategory.Id, documentCategory.Name),
+ Group group => IdentifiableName.Create(group.Id, group.Name),
+ GroupUser groupUser => IdentifiableName.Create(groupUser.Id, groupUser.Name),
+ Issue issue => new IdentifiableName(issue.Id, issue.Subject),
+ IssueAllowedStatus issueAllowedStatus => IdentifiableName.Create(issueAllowedStatus.Id, issueAllowedStatus.Name),
+ IssueCustomField issueCustomField => IdentifiableName.Create(issueCustomField.Id, issueCustomField.Name),
+ IssuePriority issuePriority => IdentifiableName.Create(issuePriority.Id, issuePriority.Name),
+ IssueStatus issueStatus => IdentifiableName.Create(issueStatus.Id, issueStatus.Name),
+ MembershipRole membershipRole => IdentifiableName.Create(membershipRole.Id, membershipRole.Name),
+ MyAccountCustomField myAccountCustomField => IdentifiableName.Create(myAccountCustomField.Id, myAccountCustomField.Name),
+ Project project => IdentifiableName.Create(project.Id, project.Name),
+ ProjectEnabledModule projectEnabledModule => IdentifiableName.Create(projectEnabledModule.Id, projectEnabledModule.Name),
+ ProjectIssueCategory projectIssueCategory => IdentifiableName.Create(projectIssueCategory.Id, projectIssueCategory.Name),
+ ProjectTimeEntryActivity projectTimeEntryActivity => IdentifiableName.Create(projectTimeEntryActivity.Id, projectTimeEntryActivity.Name),
+ ProjectTracker projectTracker => IdentifiableName.Create(projectTracker.Id, projectTracker.Name),
+ Query query => IdentifiableName.Create(query.Id, query.Name),
+ Role role => IdentifiableName.Create(role.Id, role.Name),
+ TimeEntryActivity timeEntryActivity => IdentifiableName.Create(timeEntryActivity.Id, timeEntryActivity.Name),
+ Tracker tracker => IdentifiableName.Create(tracker.Id, tracker.Name),
+ UserGroup userGroup => IdentifiableName.Create(userGroup.Id, userGroup.Name),
+ Version version => IdentifiableName.Create(version.Id, version.Name),
+ Watcher watcher => IdentifiableName.Create(watcher.Id, watcher.Name),
+ _ => null
+ };
+ }
+
+
+ ///
+ /// Converts an integer value to an object.
+ ///
+ /// An integer value representing the identifier. Must be greater than zero.
+ /// An object with the specified identifier and a null name.
+ /// Thrown when the given value is less than or equal to zero.
+ public static IdentifiableName ToIdentifier(this int val)
+ {
+ if (val <= 0)
+ {
+ throw new RedmineException(nameof(val), "Value must be greater than zero");
+ }
+
+ return new IdentifiableName(val, null);
+ }
+
+ ///
+ /// Converts an integer value into an object.
+ ///
+ /// The integer value representing the ID of an issue status.
+ /// An object initialized with the specified identifier.
+ /// Thrown when the specified value is less than or equal to zero.
+ public static IssueStatus ToIssueStatusIdentifier(this int val)
+ {
+ if (val <= 0)
+ {
+ throw new RedmineException(nameof(val), "Value must be greater than zero");
+ }
+
+ return new IssueStatus(val, null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Extensions/ListExtensions.cs b/src/redmine-net-api/Extensions/ListExtensions.cs
new file mode 100755
index 00000000..48ef0705
--- /dev/null
+++ b/src/redmine-net-api/Extensions/ListExtensions.cs
@@ -0,0 +1,129 @@
+/*
+ Copyright 2011 - 2025 Adrian Popescu
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+using System.Collections.Generic;
+
+namespace Redmine.Net.Api.Extensions
+{
+ ///
+ /// Provides extension methods for operations on lists.
+ ///
+ public static class ListExtensions
+ {
+ ///
+ /// Creates a deep clone of the specified list.
+ ///
+ /// The type of elements in the list. Must implement .
+ /// The list to be cloned.
+ /// Specifies whether to reset the ID for each cloned item.
+ /// A new list containing cloned copies of the elements from the original list. Returns null if the original list is null.
+ public static IList Clone(this IList listToClone, bool resetId) where T : ICloneable
+ {
+ if (listToClone == null)
+ {
+ return null;
+ }
+
+ var clonedList = new List(listToClone.Count);
+
+ foreach (var item in listToClone)
+ {
+ clonedList.Add(item.Clone(resetId));
+ }
+
+ return clonedList;
+ }
+
+ ///
+ /// Creates a deep clone of the specified list.
+ ///
+ /// The type of elements in the list. Must implement .
+ /// The list to be cloned.
+ /// Specifies whether to reset the ID for each cloned item.
+ /// A new list containing cloned copies of the elements from the original list. Returns null if the original list is null.
+ public static List Clone(this List listToClone, bool resetId) where T : ICloneable
+ {
+ if (listToClone == null)
+ {
+ return null;
+ }
+
+ var clonedList = new List(listToClone.Count);
+
+ foreach (var item in listToClone)
+ {
+ clonedList.Add(item.Clone(resetId));
+ }
+ return clonedList;
+ }
+
+ ///
+ /// Compares two lists for equality by checking if they contain the same elements in the same order.
+ ///
+ /// The type of elements in the lists. Must be a reference type.
+ /// The first list to be compared.
+ /// The second list to be compared.
+ /// True if both lists contain the same elements in the same order; otherwise, false. Returns false if either list is null.
+ public static bool Equals(this IList list, IList listToCompare) where T : class
+ {
+ if (list == null || listToCompare == null)
+ {
+ return false;
+ }
+
+ if (list.Count != listToCompare.Count)
+ {
+ return false;
+ }
+
+ var index = 0;
+ while (index < list.Count && list[index].Equals(listToCompare[index]))
+ {
+ index++;
+ }
+
+ return index == list.Count;
+ }
+
+ ///
+ /// Compares two lists for equality based on their elements.
+ ///
+ /// The type of elements in the lists. Must be a reference type.
+ /// The first list to compare.
+ /// The second list to compare.
+ /// True if both lists are non-null, have the same count, and all corresponding elements are equal; otherwise, false.
+ public static bool Equals(this List list, List listToCompare) where T : class
+ {
+ if (list == null || listToCompare == null)
+ {
+ return false;
+ }
+
+ if (list.Count != listToCompare.Count)
+ {
+ return false;
+ }
+
+ var index = 0;
+ while (index < list.Count && list[index].Equals(listToCompare[index]))
+ {
+ index++;
+ }
+
+ return index == list.Count;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Extensions/LoggerExtensions.cs b/src/redmine-net-api/Extensions/LoggerExtensions.cs
deleted file mode 100755
index 9e836a19..00000000
--- a/src/redmine-net-api/Extensions/LoggerExtensions.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- Copyright 2011 - 2016 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using Redmine.Net.Api.Logging;
-
-namespace Redmine.Net.Api.Extensions
-{
- ///
- ///
- public static class LoggerExtensions
- {
- ///
- /// Uses the console log.
- ///
- /// The redmine manager.
- public static void UseConsoleLog(this RedmineManager redmineManager)
- {
- Logger.UseLogger(new ConsoleLogger());
- }
-
- ///
- /// Uses the color console log.
- ///
- /// The redmine manager.
- public static void UseColorConsoleLog(this RedmineManager redmineManager)
- {
- Logger.UseLogger(new ColorConsoleLogger());
- }
-
- ///
- /// Uses the trace log.
- ///
- /// The redmine manager.
- public static void UseTraceLog(this RedmineManager redmineManager)
- {
- Logger.UseLogger(new TraceLogger());
- }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs b/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs
deleted file mode 100644
index d2b42862..00000000
--- a/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs
+++ /dev/null
@@ -1,350 +0,0 @@
-/*
-Copyright 2011 - 2025 Adrian Popescu
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-#if !(NET20)
-
-using System;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-using System.Threading;
-using System.Threading.Tasks;
-using Redmine.Net.Api.Extensions;
-using Redmine.Net.Api.Serialization;
-using Redmine.Net.Api.Types;
-
-namespace Redmine.Net.Api.Async
-{
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManger async methods instead.")]
- public static class RedmineManagerAsyncExtensions
- {
- ///
- /// Gets the current user asynchronous.
- ///
- /// The redmine manager.
- /// The parameters.
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task GetCurrentUserAsync(this RedmineManager redmineManager, NameValueCollection parameters = null, string impersonateUserName = null, CancellationToken cancellationToken = default)
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
- return await redmineManager.GetCurrentUserAsync(requestOptions, cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// Creates the or update wiki page asynchronous.
- ///
- /// The redmine manager.
- /// The project identifier.
- /// Name of the page.
- /// The wiki page.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage)
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
-
- return await redmineManager.CreateWikiPageAsync(projectId, pageName, wikiPage, requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Creates the or update wiki page asynchronous.
- ///
- /// The redmine manager.
- /// The project identifier.
- /// Name of the page.
- /// The wiki page.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage)
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
- await redmineManager.UpdateWikiPageAsync(projectId, pageName, wikiPage, requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Deletes the wiki page asynchronous.
- ///
- /// The redmine manager.
- /// The project identifier.
- /// Name of the page.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName)
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
- await redmineManager.DeleteWikiPageAsync(projectId, pageName, requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Support for adding attachments through the REST API is added in Redmine 1.4.0.
- /// Upload a file to server. This method does not block the calling thread.
- ///
- /// The redmine manager.
- /// The content of the file that will be uploaded on server.
- ///
- /// .
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task UploadFileAsync(this RedmineManager redmineManager, byte[] data)
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
- return await redmineManager.UploadFileAsync(data, null, requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Downloads the file asynchronous.
- ///
- /// The redmine manager.
- /// The address.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task DownloadFileAsync(this RedmineManager redmineManager, string address)
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
-
- return await redmineManager.DownloadFileAsync(address, requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Gets the wiki page asynchronous.
- ///
- /// The redmine manager.
- /// The project identifier.
- /// The parameters.
- /// Name of the page.
- /// The version.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, NameValueCollection parameters, string pageName, uint version = 0)
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters);
- return await redmineManager.GetWikiPageAsync(projectId, pageName, requestOptions, version).ConfigureAwait(false);
- }
-
- ///
- /// Gets all wiki pages asynchronous.
- ///
- /// The redmine manager.
- /// The parameters.
- /// The project identifier.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, NameValueCollection parameters, string projectId)
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters);
- return await redmineManager.GetAllWikiPagesAsync(projectId, requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Adds an existing user to a group. This method does not block the calling thread.
- ///
- /// The redmine manager.
- /// The group id.
- /// The user id.
- ///
- /// Returns the Guid associated with the async request.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId)
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
- await redmineManager.AddUserToGroupAsync(groupId, userId, requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Removes an user from a group. This method does not block the calling thread.
- ///
- /// The redmine manager.
- /// The group id.
- /// The user id.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId)
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
- await redmineManager.RemoveUserFromGroupAsync(groupId, userId, requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Adds the watcher asynchronous.
- ///
- /// The redmine manager.
- /// The issue identifier.
- /// The user identifier.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId)
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
- await redmineManager.AddWatcherToIssueAsync(issueId, userId, requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Removes the watcher asynchronous.
- ///
- /// The redmine manager.
- /// The issue identifier.
- /// The user identifier.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId)
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
- await redmineManager.RemoveWatcherFromIssueAsync(issueId, userId, requestOptions).ConfigureAwait(false);
- }
-
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")]
- public static async Task CountAsync(this RedmineManager redmineManager, params string[] include) where T : class, new()
- {
- return await redmineManager.CountAsync(null, CancellationToken.None).ConfigureAwait(false);
- }
-
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use CountAsync method instead.")]
- public static async Task CountAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new()
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters);
- return await redmineManager.CountAsync(requestOptions).ConfigureAwait(false);
- }
-
-
- ///
- /// Gets the paginated objects asynchronous.
- ///
- ///
- /// The redmine manager.
- /// The parameters.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use GetPagedAsync method instead.")]
- public static async Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters)
- where T : class, new()
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters);
- return await redmineManager.GetPagedAsync(requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Gets the objects asynchronous.
- ///
- ///
- /// The redmine manager.
- /// The parameters.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use GetAsync method instead.")]
- public static async Task> GetObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters)
- where T : class, new()
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters);
- return await redmineManager.GetAsync(requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Gets a Redmine object. This method does not block the calling thread.
- ///
- /// The type of objects to retrieve.
- /// The redmine manager.
- /// The id of the object.
- /// Optional filters and/or optional fetched data.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use GetAsync method instead.")]
- public static async Task GetObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters)
- where T : class, new()
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters);
- return await redmineManager.GetAsync(id, requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Creates a new Redmine object. This method does not block the calling thread.
- ///
- /// The type of object to create.
- /// The redmine manager.
- /// The object to create.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use CreateAsync method instead.")]
- public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity)
- where T : class, new()
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
- return await redmineManager.CreateAsync(entity, null, requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Creates a new Redmine object. This method does not block the calling thread.
- ///
- /// The type of object to create.
- /// The redmine manager.
- /// The object to create.
- /// The owner identifier.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use CreateAsync method instead.")]
- public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity, string ownerId)
- where T : class, new()
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
- return await redmineManager.CreateAsync(entity, ownerId, requestOptions, CancellationToken.None).ConfigureAwait(false);
- }
-
- ///
- /// Updates the object asynchronous.
- ///
- ///
- /// The redmine manager.
- /// The identifier.
- /// The object.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use UpdateAsync method instead.")]
- public static async Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity)
- where T : class, new()
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
- await redmineManager.UpdateAsync(id, entity, requestOptions).ConfigureAwait(false);
- }
-
- ///
- /// Deletes the Redmine object. This method does not block the calling thread.
- ///
- /// The type of objects to delete.
- /// The redmine manager.
- /// The id of the object to delete
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use DeleteAsync method instead.")]
- public static async Task DeleteObjectAsync(this RedmineManager redmineManager, string id)
- where T : class, new()
- {
- var requestOptions = RedmineManagerExtensions.CreateRequestOptions();
- await redmineManager.DeleteAsync(id, requestOptions).ConfigureAwait(false);
- }
- }
-}
-#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs
index fb94205f..71ebcf13 100644
--- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs
+++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs
@@ -17,13 +17,15 @@ limitations under the License.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
-using System.Globalization;
+using Redmine.Net.Api.Common;
#if !(NET20)
using System.Threading;
using System.Threading.Tasks;
#endif
using Redmine.Net.Api.Exceptions;
-using Redmine.Net.Api.Net;
+using Redmine.Net.Api.Http;
+using Redmine.Net.Api.Http.Extensions;
+using Redmine.Net.Api.Net.Internal;
using Redmine.Net.Api.Serialization;
using Redmine.Net.Api.Types;
@@ -35,132 +37,124 @@ namespace Redmine.Net.Api.Extensions
public static class RedmineManagerExtensions
{
///
- ///
+ /// Archives a project in Redmine based on the specified project identifier.
///
- ///
- ///
- ///
- ///
- public static void ArchiveProject(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage the API requests.
+ /// The unique identifier of the project to be archived.
+ /// Additional request options to include in the API call.
+ public static void ArchiveProject(this RedmineManager redmineManager, string projectIdentifier,
+ RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.ProjectArchive(projectIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- redmineManager.ApiClient.Update(escapedUri, string.Empty ,requestOptions);
+ redmineManager.ApiClient.Update(uri, string.Empty ,requestOptions);
}
-
+
///
- ///
+ /// Unarchives a project in Redmine based on the specified project identifier.
///
- ///
- ///
- ///
- ///
- public static void UnarchiveProject(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage the API requests.
+ /// The unique identifier of the project to be unarchived.
+ /// Additional request options to include in the API call.
+ public static void UnarchiveProject(this RedmineManager redmineManager, string projectIdentifier,
+ RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.ProjectUnarchive(projectIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- redmineManager.ApiClient.Update(escapedUri, string.Empty ,requestOptions);
+ redmineManager.ApiClient.Update(uri, string.Empty ,requestOptions);
}
-
+
///
- ///
+ /// Reopens a previously closed project in Redmine based on the specified project identifier.
///
- ///
- ///
- ///
- ///
- public static void ReopenProject(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to execute the API request.
+ /// The unique identifier of the project to be reopened.
+ /// Additional request options to include in the API call.
+ public static void ReopenProject(this RedmineManager redmineManager, string projectIdentifier,
+ RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.ProjectReopen(projectIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- redmineManager.ApiClient.Update(escapedUri, string.Empty ,requestOptions);
+ redmineManager.ApiClient.Update(uri, string.Empty ,requestOptions);
}
-
+
///
- ///
+ /// Closes a project in Redmine based on the specified project identifier.
///
- ///
- ///
- ///
- ///
- public static void CloseProject(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage the API requests.
+ /// The unique identifier of the project to be closed.
+ /// Additional request options to include in the API call.
+ public static void CloseProject(this RedmineManager redmineManager, string projectIdentifier,
+ RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.ProjectClose(projectIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- redmineManager.ApiClient.Update(escapedUri,string.Empty, requestOptions);
+ redmineManager.ApiClient.Update(uri,string.Empty, requestOptions);
}
-
+
///
- ///
+ /// Adds a related issue to a project repository in Redmine based on the specified parameters.
///
- ///
- ///
- ///
- ///
- ///
- public static void ProjectRepositoryAddRelatedIssue(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage the API requests.
+ /// The unique identifier of the project to which the repository belongs.
+ /// The unique identifier of the repository within the project.
+ /// The revision or commit ID to relate the issue to.
+ /// Additional request options to include in the API call.
+ public static void ProjectRepositoryAddRelatedIssue(this RedmineManager redmineManager,
+ string projectIdentifier, string repositoryIdentifier, string revision,
+ RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.ProjectRepositoryAddRelatedIssue(projectIdentifier, repositoryIdentifier, revision);
- var escapedUri = Uri.EscapeDataString(uri);
-
- _ = redmineManager.ApiClient.Create(escapedUri,string.Empty, requestOptions);
+ _ = redmineManager.ApiClient.Create(uri,string.Empty, requestOptions);
}
///
- ///
+ /// Removes a related issue from the specified repository revision of a project in Redmine.
///
- ///
- ///
- ///
- ///
- ///
- ///
- public static void ProjectRepositoryRemoveRelatedIssue(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, string issueIdentifier, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage the API requests.
+ /// The unique identifier of the project containing the repository.
+ /// The unique identifier of the repository from which the related issue will be removed.
+ /// The specific revision of the repository to disassociate the issue from.
+ /// The unique identifier of the issue to be removed as related.
+ /// Additional request options to include in the API call.
+ public static void ProjectRepositoryRemoveRelatedIssue(this RedmineManager redmineManager,
+ string projectIdentifier, string repositoryIdentifier, string revision, string issueIdentifier,
+ RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.ProjectRepositoryRemoveRelatedIssue(projectIdentifier, repositoryIdentifier, revision, issueIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- _ = redmineManager.ApiClient.Delete(escapedUri, requestOptions);
+ _ = redmineManager.ApiClient.Delete(uri, requestOptions);
}
-
+
///
- ///
+ /// Retrieves a paginated list of news for a specific project in Redmine.
///
- ///
- ///
- ///
- ///
- public static PagedResults GetProjectNews(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage the API requests.
+ /// The unique identifier of the project for which news is being retrieved.
+ /// Additional request options to include in the API call, if any.
+ /// A paginated list of news items associated with the specified project.
+ public static PagedResults GetProjectNews(this RedmineManager redmineManager, string projectIdentifier,
+ RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- var response = redmineManager.GetPaginatedInternal(escapedUri, requestOptions);
+ var response = redmineManager.GetPaginatedInternal(uri, requestOptions);
return response;
}
///
- ///
+ /// Adds a news item to a project in Redmine based on the specified project identifier.
///
- ///
- ///
- ///
- ///
- ///
- ///
- public static News AddProjectNews(this RedmineManager redmineManager, string projectIdentifier, News news, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage the API requests.
+ /// The unique identifier of the project to which the news will be added.
+ /// The news item to be added to the project, which must contain a valid title.
+ /// Additional request options to include in the API call.
+ /// The created news item as a response from the Redmine server.
+ /// Thrown when the provided news object is null or the news title is blank.
+ public static News AddProjectNews(this RedmineManager redmineManager, string projectIdentifier, News news,
+ RequestOptions requestOptions = null)
{
if (news == null)
{
@@ -174,24 +168,23 @@ public static News AddProjectNews(this RedmineManager redmineManager, string pro
var payload = redmineManager.Serializer.Serialize(news);
- var uri = Uri.EscapeDataString(redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier));
-
- var escapedUri = Uri.EscapeDataString(uri);
+ var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier);
- var response = redmineManager.ApiClient.Create(escapedUri, payload, requestOptions);
+ var response = redmineManager.ApiClient.Create(uri, payload, requestOptions);
return response.DeserializeTo(redmineManager.Serializer);
}
///
- ///
+ /// Retrieves the memberships associated with the specified project in Redmine.
///
- ///
- ///
- ///
- ///
- ///
- public static PagedResults GetProjectMemberships(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage the API requests.
+ /// The unique identifier of the project for which memberships are being retrieved.
+ /// Additional request options to include in the API call, such as pagination or filters.
+ /// Returns a paginated collection of project memberships for the specified project.
+ /// Thrown when the API request fails or an error occurs during execution.
+ public static PagedResults GetProjectMemberships(this RedmineManager redmineManager,
+ string projectIdentifier, RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.ProjectMemberships(projectIdentifier);
@@ -201,14 +194,15 @@ public static PagedResults GetProjectMemberships(this Redmine
}
///
- ///
+ /// Retrieves the list of files associated with a specific project in Redmine.
///
- ///
- ///
- ///
- ///
- ///
- public static PagedResults GetProjectFiles(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage the API requests.
+ /// The unique identifier of the project whose files are being retrieved.
+ /// Additional request options to include in the API call.
+ /// A paginated result containing the list of files associated with the project.
+ /// Thrown when the API request fails or returns an error response.
+ public static PagedResults GetProjectFiles(this RedmineManager redmineManager, string projectIdentifier,
+ RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.ProjectFilesFragment(projectIdentifier);
@@ -220,9 +214,9 @@ public static PagedResults GetProjectFiles(this RedmineManager redmineMana
///
/// Returns the user whose credentials are used to access the API.
///
- ///
- ///
- ///
+ /// The instance of the RedmineManager used to manage the API requests.
+ /// Additional request options to include in the API call.
+ /// The authenticated user as a object.
public static User GetCurrentUser(this RedmineManager redmineManager, RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.CurrentUser();
@@ -231,11 +225,13 @@ public static User GetCurrentUser(this RedmineManager redmineManager, RequestOpt
return response.DeserializeTo(redmineManager.Serializer);
}
-
+
///
- ///
+ /// Retrieves the account details of the currently authenticated user.
///
- /// Returns the my account details.
+ /// The instance of the RedmineManager used to perform the API call.
+ /// Optional configuration for the API request.
+ /// Returns the account details of the authenticated user as a MyAccount object.
public static MyAccount GetMyAccount(this RedmineManager redmineManager, RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.MyAccount();
@@ -246,15 +242,16 @@ public static MyAccount GetMyAccount(this RedmineManager redmineManager, Request
}
///
- /// Adds the watcher to issue.
+ /// Adds a watcher to a specific issue in Redmine using the specified issue ID and user ID.
///
- ///
- /// The issue identifier.
- /// The user identifier.
- ///
- public static void AddWatcherToIssue(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage the API requests.
+ /// The unique identifier of the issue to which the watcher will be added.
+ /// The unique identifier of the user to be added as a watcher.
+ /// Additional request options to include in the API call.
+ public static void AddWatcherToIssue(this RedmineManager redmineManager, int issueId, int userId,
+ RequestOptions requestOptions = null)
{
- var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToString(CultureInfo.InvariantCulture));
+ var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToInvariantString());
var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer);
@@ -262,29 +259,31 @@ public static void AddWatcherToIssue(this RedmineManager redmineManager, int iss
}
///
- /// Removes the watcher from issue.
+ /// Removes a watcher from a specific issue in Redmine based on the specified issue identifier and user identifier.
///
- ///
- /// The issue identifier.
- /// The user identifier.
- ///
- public static void RemoveWatcherFromIssue(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage the API requests.
+ /// The unique identifier of the issue from which the watcher will be removed.
+ /// The unique identifier of the user to be removed as a watcher.
+ /// Additional request options to include in the API call.
+ public static void RemoveWatcherFromIssue(this RedmineManager redmineManager, int issueId, int userId,
+ RequestOptions requestOptions = null)
{
- var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture));
+ var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToInvariantString(), userId.ToInvariantString());
redmineManager.ApiClient.Delete(uri, requestOptions);
}
///
- /// Adds an existing user to a group.
+ /// Adds a user to a specified group in Redmine.
///
- ///
- /// The group id.
- /// The user id.
- ///
- public static void AddUserToGroup(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage API requests.
+ /// The unique identifier of the group to which the user will be added.
+ /// The unique identifier of the user to be added to the group.
+ /// Additional request options to include in the API call.
+ public static void AddUserToGroup(this RedmineManager redmineManager, int groupId, int userId,
+ RequestOptions requestOptions = null)
{
- var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToString(CultureInfo.InvariantCulture));
+ var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToInvariantString());
var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer);
@@ -292,29 +291,30 @@ public static void AddUserToGroup(this RedmineManager redmineManager, int groupI
}
///
- /// Removes an user from a group.
+ /// Removes a user from a specified group in Redmine.
///
- ///
- /// The group id.
- /// The user id.
- ///
- public static void RemoveUserFromGroup(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage API requests.
+ /// The unique identifier of the group from which the user will be removed.
+ /// The unique identifier of the user to be removed from the group.
+ /// Additional request options to customize the API call.
+ public static void RemoveUserFromGroup(this RedmineManager redmineManager, int groupId, int userId,
+ RequestOptions requestOptions = null)
{
- var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture));
+ var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToInvariantString(), userId.ToInvariantString());
redmineManager.ApiClient.Delete(uri, requestOptions);
}
///
- /// Creates or updates a wiki page.
+ /// Updates a specified wiki page for a project in Redmine.
///
- ///
- /// The project id or identifier.
- /// The wiki page name.
- /// The wiki page to create or update.
- ///
- ///
- public static void UpdateWikiPage(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to process the request.
+ /// The unique identifier of the project containing the wiki page.
+ /// The name of the wiki page to be updated.
+ /// The WikiPage object containing the updated data for the page.
+ /// Optional parameters for customizing the API request.
+ public static void UpdateWikiPage(this RedmineManager redmineManager, string projectId, string pageName,
+ WikiPage wikiPage, RequestOptions requestOptions = null)
{
var payload = redmineManager.Serializer.Serialize(wikiPage);
@@ -325,21 +325,21 @@ public static void UpdateWikiPage(this RedmineManager redmineManager, string pro
var uri = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName);
- var escapedUri = Uri.EscapeDataString(uri);
-
- redmineManager.ApiClient.Patch(escapedUri, payload, requestOptions);
+ redmineManager.ApiClient.Patch(uri, payload, requestOptions);
}
///
- ///
+ /// Creates a new wiki page within a specified project in Redmine.
///
- ///
- ///
- ///
- ///
- ///
- ///
- public static WikiPage CreateWikiPage(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage API requests.
+ /// The unique identifier of the project where the wiki page will be created.
+ /// The name of the new wiki page.
+ /// The WikiPage object containing the content and metadata for the new page.
+ /// Additional request options to include in the API call.
+ /// The created WikiPage object containing the details of the new wiki page.
+ /// Thrown when the request payload is empty or if the API request fails.
+ public static WikiPage CreateWikiPage(this RedmineManager redmineManager, string projectId, string pageName,
+ WikiPage wikiPage, RequestOptions requestOptions = null)
{
var payload = redmineManager.Serializer.Serialize(wikiPage);
@@ -350,43 +350,41 @@ public static WikiPage CreateWikiPage(this RedmineManager redmineManager, string
var uri = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName);
- var escapedUri = Uri.EscapeDataString(uri);
-
- var response = redmineManager.ApiClient.Create(escapedUri, payload, requestOptions);
+ var response = redmineManager.ApiClient.Update(uri, payload, requestOptions);
return response.DeserializeTo(redmineManager.Serializer);
}
///
- /// Gets the wiki page.
+ /// Retrieves a wiki page from a Redmine project using the specified project identifier and page name.
///
- ///
- /// The project identifier.
- /// Name of the page.
- ///
- /// The version.
- ///
- public static WikiPage GetWikiPage(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null, uint version = 0)
+ /// The instance of the RedmineManager responsible for managing API requests.
+ /// The unique identifier of the project containing the wiki page.
+ /// The name of the wiki page to retrieve.
+ /// Additional options to include in the API request, such as headers or query parameters.
+ /// The specific version of the wiki page to retrieve. If 0, the latest version is retrieved.
+ /// A WikiPage object containing the details of the requested wiki page.
+ public static WikiPage GetWikiPage(this RedmineManager redmineManager, string projectId, string pageName,
+ RequestOptions requestOptions = null, uint version = 0)
{
var uri = version == 0
? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName)
- : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToString(CultureInfo.InvariantCulture));
+ : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToInvariantString());
- var escapedUri = Uri.EscapeDataString(uri);
-
- var response = redmineManager.ApiClient.Get(escapedUri, requestOptions);
+ var response = redmineManager.ApiClient.Get(uri, requestOptions);
return response.DeserializeTo(redmineManager.Serializer);
}
///
- /// Returns the list of all pages in a project wiki.
+ /// Retrieves all wiki pages associated with the specified project.
///
- ///
- /// The project id or identifier.
- ///
- ///
- public static List GetAllWikiPages(this RedmineManager redmineManager, string projectId, RequestOptions requestOptions = null)
+ /// The instance of the RedmineManager used to manage API requests.
+ /// The unique identifier of the project whose wiki pages are to be fetched.
+ /// Additional request options to include in the API call.
+ /// A list of wiki pages associated with the specified project.
+ public static List GetAllWikiPages(this RedmineManager redmineManager, string projectId,
+ RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.ProjectWikiIndex(projectId);
@@ -399,17 +397,15 @@ public static List GetAllWikiPages(this RedmineManager redmineManager,
/// Deletes a wiki page, its attachments and its history. If the deleted page is a parent page, its child pages are not
/// deleted but changed as root pages.
///
- ///
+ /// The instance of the RedmineManager used to manage API requests.
/// The project id or identifier.
/// The wiki page name.
- ///
+ /// Additional request options to include in the API call.
public static void DeleteWikiPage(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null)
{
var uri = redmineManager.RedmineApiUrls.ProjectWikiPageDelete(projectId, pageName);
- var escapedUri = Uri.EscapeDataString(uri);
-
- redmineManager.ApiClient.Delete(escapedUri, requestOptions);
+ redmineManager.ApiClient.Delete(uri, requestOptions);
}
///
@@ -418,7 +414,7 @@ public static void DeleteWikiPage(this RedmineManager redmineManager, string pro
///
/// The issue identifier.
/// The attachment.
- ///
+ /// Additional request options to include in the API call.
public static void UpdateIssueAttachment(this RedmineManager redmineManager, int issueId, Attachment attachment, RequestOptions requestOptions = null)
{
var attachments = new Attachments
@@ -428,7 +424,7 @@ public static void UpdateIssueAttachment(this RedmineManager redmineManager, int
var data = redmineManager.Serializer.Serialize(attachments);
- var uri = redmineManager.RedmineApiUrls.AttachmentUpdate(issueId.ToString(CultureInfo.InvariantCulture));
+ var uri = redmineManager.RedmineApiUrls.AttachmentUpdate(issueId.ToInvariantString());
redmineManager.ApiClient.Patch(uri, data, requestOptions);
}
@@ -449,7 +445,11 @@ public static PagedResults Search(this RedmineManager redmineManager, st
{
var parameters = CreateSearchParameters(q, limit, offset, searchFilter);
- var response = redmineManager.GetPaginated(new RequestOptions() {QueryString = parameters});
+ var response = redmineManager.GetPaginated(new RequestOptions
+ {
+ QueryString = parameters,
+ ImpersonateUser = impersonateUserName
+ });
return response;
}
@@ -464,8 +464,8 @@ private static NameValueCollection CreateSearchParameters(string q, int limit, i
var parameters = new NameValueCollection
{
{RedmineKeys.Q, q},
- {RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)},
- {RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)},
+ {RedmineKeys.LIMIT, limit.ToInvariantString()},
+ {RedmineKeys.OFFSET, offset.ToInvariantString()},
};
return searchFilter != null ? searchFilter.Build(parameters) : parameters;
@@ -478,15 +478,13 @@ private static NameValueCollection CreateSearchParameters(string q, int limit, i
///
///
///
- ///
+ /// Additional request options to include in the API call.
///
public static async Task ArchiveProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
{
var uri = redmineManager.RedmineApiUrls.ProjectArchive(projectIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- await redmineManager.ApiClient.DeleteAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false);
+ await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false);
}
///
@@ -494,15 +492,13 @@ public static async Task ArchiveProjectAsync(this RedmineManager redmineManager,
///
///
///
- ///
+ /// Additional request options to include in the API call.
///
public static async Task UnarchiveProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
{
var uri = redmineManager.RedmineApiUrls.ProjectUnarchive(projectIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- await redmineManager.ApiClient.DeleteAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false);
+ await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false);
}
///
@@ -510,15 +506,13 @@ public static async Task UnarchiveProjectAsync(this RedmineManager redmineManage
///
///
///
- ///
+ /// Additional request options to include in the API call.
///
public static async Task CloseProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
{
var uri = redmineManager.RedmineApiUrls.ProjectClose(projectIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- await redmineManager.ApiClient.UpdateAsync(escapedUri, string.Empty, requestOptions, cancellationToken).ConfigureAwait(false);
+ await redmineManager.ApiClient.UpdateAsync(uri, string.Empty, requestOptions, cancellationToken).ConfigureAwait(false);
}
///
@@ -526,15 +520,13 @@ public static async Task CloseProjectAsync(this RedmineManager redmineManager, s
///
///
///
- ///
+ /// Additional request options to include in the API call.
///
public static async Task ReopenProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
{
var uri = redmineManager.RedmineApiUrls.ProjectReopen(projectIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- await redmineManager.ApiClient.UpdateAsync(escapedUri, string.Empty, requestOptions, cancellationToken).ConfigureAwait(false);
+ await redmineManager.ApiClient.UpdateAsync(uri, string.Empty, requestOptions, cancellationToken).ConfigureAwait(false);
}
///
@@ -544,15 +536,13 @@ public static async Task ReopenProjectAsync(this RedmineManager redmineManager,
///
///
///
- ///
+ /// Additional request options to include in the API call.
///
public static async Task ProjectRepositoryAddRelatedIssueAsync(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
{
var uri = redmineManager.RedmineApiUrls.ProjectRepositoryAddRelatedIssue(projectIdentifier, repositoryIdentifier, revision);
- var escapedUri = Uri.EscapeDataString(uri);
-
- await redmineManager.ApiClient.CreateAsync(escapedUri, string.Empty ,requestOptions, cancellationToken).ConfigureAwait(false);
+ await redmineManager.ApiClient.CreateAsync(uri, string.Empty ,requestOptions, cancellationToken).ConfigureAwait(false);
}
///
@@ -563,15 +553,13 @@ public static async Task ProjectRepositoryAddRelatedIssueAsync(this RedmineManag
///
///
///
- ///
+ /// Additional request options to include in the API call.
///
public static async Task ProjectRepositoryRemoveRelatedIssueAsync(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, string issueIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
{
var uri = redmineManager.RedmineApiUrls.ProjectRepositoryRemoveRelatedIssue(projectIdentifier, repositoryIdentifier, revision, issueIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- await redmineManager.ApiClient.DeleteAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false);
+ await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false);
}
///
@@ -579,16 +567,14 @@ public static async Task ProjectRepositoryRemoveRelatedIssueAsync(this RedmineMa
///
///
///
- ///
+ /// Additional request options to include in the API call.
///
///
public static async Task> GetProjectNewsAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
{
var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- var response = await redmineManager.ApiClient.GetPagedAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false);
+ var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false);
return response.DeserializeToPagedResults(redmineManager.Serializer);
}
@@ -599,7 +585,7 @@ public static async Task> GetProjectNewsAsync(this RedmineMan
///
///
///
- ///
+ /// Additional request options to include in the API call.
///
///
///
@@ -617,11 +603,9 @@ public static async Task AddProjectNewsAsync(this RedmineManager redmineMa
var payload = redmineManager.Serializer.Serialize(news);
- var uri = Uri.EscapeDataString(redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier));
+ var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier);
- var escapedUri = Uri.EscapeDataString(uri);
-
- var response = await redmineManager.ApiClient.CreateAsync(escapedUri, payload, requestOptions, cancellationToken).ConfigureAwait(false);
+ var response = await redmineManager.ApiClient.CreateAsync(uri, payload, requestOptions, cancellationToken).ConfigureAwait(false);
return response.DeserializeTo(redmineManager.Serializer);
}
@@ -631,7 +615,7 @@ public static async Task AddProjectNewsAsync(this RedmineManager redmineMa
///
///
///
- ///
+ /// Additional request options to include in the API call.
///
///
///
@@ -649,7 +633,7 @@ public static async Task> GetProjectMembershipsA
///
///
///
- ///
+ /// Additional request options to include in the API call.
///
///
///
@@ -661,7 +645,6 @@ public static async Task> GetProjectFilesAsync(this RedmineMa
return response.DeserializeToPagedResults(redmineManager.Serializer);
}
-
///
///
@@ -673,23 +656,28 @@ public static async Task> GetProjectFilesAsync(this RedmineMa
///
///
///
- public static async Task> SearchAsync(this RedmineManager redmineManager, string q, int limit = RedmineManager.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null, CancellationToken cancellationToken = default)
+ public static async Task> SearchAsync(this RedmineManager redmineManager,
+ string q,
+ int limit = RedmineConstants.DEFAULT_PAGE_SIZE_VALUE,
+ int offset = 0,
+ SearchFilterBuilder searchFilter = null,
+ CancellationToken cancellationToken = default)
{
var parameters = CreateSearchParameters(q, limit, offset, searchFilter);
- var response = await redmineManager.ApiClient.GetPagedAsync("", new RequestOptions()
+ var response = await redmineManager.GetPagedAsync(new RequestOptions()
{
QueryString = parameters
}, cancellationToken).ConfigureAwait(false);
- return response.DeserializeToPagedResults(redmineManager.Serializer);
+ return response;
}
///
///
///
///
- ///
+ /// Additional request options to include in the API call.
///
///
public static async Task GetCurrentUserAsync(this RedmineManager redmineManager, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
@@ -701,6 +689,22 @@ public static async Task GetCurrentUserAsync(this RedmineManager redmineMa
return response.DeserializeTo(redmineManager.Serializer);
}
+ ///
+ /// Retrieves the account details of the currently authenticated user.
+ ///
+ /// The instance of the RedmineManager used to perform the API call.
+ /// Optional configuration for the API request.
+ ///
+ /// Returns the account details of the authenticated user as a MyAccount object.
+ public static async Task GetMyAccountAsync(this RedmineManager redmineManager, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
+ {
+ var uri = redmineManager.RedmineApiUrls.MyAccount();
+
+ var response = await redmineManager.ApiClient.GetAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false);
+
+ return response.DeserializeTo(redmineManager.Serializer);
+ }
+
///
/// Creates or updates wiki page asynchronous.
///
@@ -708,23 +712,26 @@ public static async Task GetCurrentUserAsync(this RedmineManager redmineMa
/// The project identifier.
/// Name of the page.
/// The wiki page.
- ///
+ /// Additional request options to include in the API call.
///
///
public static async Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
{
var payload = redmineManager.Serializer.Serialize(wikiPage);
+ if (pageName.IsNullOrWhiteSpace())
+ {
+ throw new RedmineException("Page name cannot be blank");
+ }
+
if (string.IsNullOrEmpty(payload))
{
throw new RedmineException("The payload is empty");
}
- var url = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName);
+ var path = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName);
- var escapedUri = Uri.EscapeDataString(url);
-
- var response = await redmineManager.ApiClient.CreateAsync(escapedUri, payload,requestOptions, cancellationToken).ConfigureAwait(false);
+ var response = await redmineManager.ApiClient.UpdateAsync(path, payload, requestOptions, cancellationToken).ConfigureAwait(false);
return response.DeserializeTo(redmineManager.Serializer);
}
@@ -736,7 +743,7 @@ public static async Task CreateWikiPageAsync(this RedmineManager redmi
/// The project identifier.
/// Name of the page.
/// The wiki page.
- ///
+ /// Additional request options to include in the API call.
///
///
public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
@@ -750,9 +757,7 @@ public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager,
var url = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName);
- var escapedUri = Uri.EscapeDataString(url);
-
- await redmineManager.ApiClient.PatchAsync(escapedUri, payload, requestOptions, cancellationToken).ConfigureAwait(false);
+ await redmineManager.ApiClient.PatchAsync(url, payload, requestOptions, cancellationToken).ConfigureAwait(false);
}
///
@@ -761,16 +766,14 @@ public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager,
/// The redmine manager.
/// The project identifier.
/// Name of the page.
- ///
+ /// Additional request options to include in the API call.
///
///
public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
{
var uri = redmineManager.RedmineApiUrls.ProjectWikiPageDelete(projectId, pageName);
- var escapedUri = Uri.EscapeDataString(uri);
-
- await redmineManager.ApiClient.DeleteAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false);
+ await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false);
}
///
@@ -779,7 +782,7 @@ public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager,
/// The redmine manager.
/// The project identifier.
/// Name of the page.
- ///
+ /// Additional request options to include in the API call.
/// The version.
///
///
@@ -787,11 +790,9 @@ public static async Task GetWikiPageAsync(this RedmineManager redmineM
{
var uri = version == 0
? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName)
- : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToString(CultureInfo.InvariantCulture));
+ : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToInvariantString());
- var escapedUri = Uri.EscapeDataString(uri);
-
- var response = await redmineManager.ApiClient.GetAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false);
+ var response = await redmineManager.ApiClient.GetAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false);
return response.DeserializeTo(redmineManager.Serializer);
}
@@ -801,7 +802,7 @@ public static async Task GetWikiPageAsync(this RedmineManager redmineM
///
/// The redmine manager.
/// The project identifier.
- ///
+ /// Additional request options to include in the API call.
///
///
public static async Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, string projectId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
@@ -810,7 +811,8 @@ public static async Task> GetAllWikiPagesAsync(this RedmineManage
var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false);
- return response.DeserializeToList(redmineManager.Serializer);
+ var pages = response.DeserializeToPagedResults(redmineManager.Serializer);
+ return pages.Items as List;
}
///
@@ -819,14 +821,14 @@ public static async Task> GetAllWikiPagesAsync(this RedmineManage
/// The redmine manager.
/// The group id.
/// The user id.
- ///
+ /// Additional request options to include in the API call.
///
///
/// Returns the Guid associated with the async request.
///
public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
{
- var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToString(CultureInfo.InvariantCulture));
+ var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToInvariantString());
var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer);
@@ -839,12 +841,12 @@ public static async Task AddUserToGroupAsync(this RedmineManager redmineManager,
/// The redmine manager.
/// The group id.
/// The user id.
- ///
+ /// Additional request options to include in the API call.
///
///
public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
{
- var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture));
+ var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToInvariantString(), userId.ToInvariantString());
await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false);
}
@@ -855,12 +857,12 @@ public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineMan
/// The redmine manager.
/// The issue identifier.
/// The user identifier.
- ///
+ /// Additional request options to include in the API call.
///
///
public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null , CancellationToken cancellationToken = default)
{
- var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToString(CultureInfo.InvariantCulture));
+ var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToInvariantString());
var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer);
@@ -873,37 +875,15 @@ public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManag
/// The redmine manager.
/// The issue identifier.
/// The user identifier.
- ///
+ /// Additional request options to include in the API call.
///
///
public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
{
- var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture));
+ var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToInvariantString(), userId.ToInvariantString());
await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false);
}
#endif
-
- internal static RequestOptions CreateRequestOptions(NameValueCollection parameters = null, string impersonateUserName = null)
- {
- RequestOptions requestOptions = null;
- if (parameters != null)
- {
- requestOptions = new RequestOptions()
- {
- QueryString = parameters
- };
- }
-
- if (impersonateUserName.IsNullOrWhiteSpace())
- {
- return requestOptions;
- }
-
- requestOptions ??= new RequestOptions();
- requestOptions.ImpersonateUser = impersonateUserName;
-
- return requestOptions;
- }
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs
index 03fdc059..a881f14d 100644
--- a/src/redmine-net-api/Extensions/StringExtensions.cs
+++ b/src/redmine-net-api/Extensions/StringExtensions.cs
@@ -28,10 +28,10 @@ namespace Redmine.Net.Api.Extensions
public static class StringExtensions
{
///
- ///
+ /// Determines whether a string is null, empty, or consists only of white-space characters.
///
- ///
- ///
+ /// The string to evaluate.
+ /// True if the string is null, empty, or whitespace; otherwise, false.
public static bool IsNullOrWhiteSpace(this string value)
{
if (value == null)
@@ -39,9 +39,9 @@ public static bool IsNullOrWhiteSpace(this string value)
return true;
}
- for (var index = 0; index < value.Length; ++index)
+ foreach (var ch in value)
{
- if (!char.IsWhiteSpace(value[index]))
+ if (!char.IsWhiteSpace(ch))
{
return false;
}
@@ -51,11 +51,11 @@ public static bool IsNullOrWhiteSpace(this string value)
}
///
- ///
+ /// Truncates a string to the specified maximum length if it exceeds that length.
///
- ///
- ///
- ///
+ /// The string to truncate.
+ /// The maximum allowed length for the string.
+ /// The truncated string if its length exceeds the maximum length; otherwise, the original string.
public static string Truncate(this string text, int maximumLength)
{
if (text.IsNullOrWhiteSpace() || maximumLength < 1 || text.Length <= maximumLength)
@@ -99,14 +99,19 @@ internal static SecureString ToSecureString(this string value)
var rv = new SecureString();
- for (var index = 0; index < value.Length; ++index)
+ foreach (var ch in value)
{
- rv.AppendChar(value[index]);
+ rv.AppendChar(ch);
}
return rv;
}
+ ///
+ /// Removes the trailing slash ('/' or '\') from the end of the string if it exists.
+ ///
+ /// The string to process.
+ /// The input string without a trailing slash, or the original string if no trailing slash exists.
internal static string RemoveTrailingSlash(this string s)
{
if (string.IsNullOrEmpty(s))
@@ -128,12 +133,24 @@ internal static string RemoveTrailingSlash(this string s)
return s;
}
-
+
+ ///
+ /// Returns the specified string value if it is neither null, empty, nor consists only of white-space characters; otherwise, returns the fallback string.
+ ///
+ /// The primary string value to evaluate.
+ /// The fallback string to return if the primary string is null, empty, or consists of only white-space characters.
+ /// The original string if it is valid; otherwise, the fallback string.
internal static string ValueOrFallback(this string value, string fallback)
{
return !value.IsNullOrWhiteSpace() ? value : fallback;
}
-
+
+ ///
+ /// Converts a value of a struct type to its invariant culture string representation.
+ ///
+ /// The struct type of the value.
+ /// The value to convert to a string.
+ /// The invariant culture string representation of the value.
internal static string ToInvariantString(this T value) where T : struct
{
return value switch
@@ -152,16 +169,22 @@ internal static string ToInvariantString(this T value) where T : struct
TimeSpan ts => ts.ToString(),
DateTime d => d.ToString(CultureInfo.InvariantCulture),
#pragma warning disable CA1308
- bool b => b.ToString().ToLowerInvariant(),
+ bool b => b ? "true" : "false",
#pragma warning restore CA1308
_ => value.ToString(),
};
}
- private const string CRLR = "\r\n";
private const string CR = "\r";
private const string LR = "\n";
-
+ private const string CRLR = $"{CR}{LR}";
+
+ ///
+ /// Replaces all line endings in the input string with the specified replacement string.
+ ///
+ /// The string in which line endings will be replaced.
+ /// The string to replace line endings with. Defaults to a combination of carriage return and line feed.
+ /// The input string with all line endings replaced by the specified replacement string.
internal static string ReplaceEndings(this string input, string replacement = CRLR)
{
if (input.IsNullOrWhiteSpace())
@@ -170,9 +193,9 @@ internal static string ReplaceEndings(this string input, string replacement = CR
}
#if NET6_0_OR_GREATER
- input = input.ReplaceLineEndings(CRLR);
+ input = input.ReplaceLineEndings(replacement);
#else
- input = Regex.Replace(input, $"{CRLR}|{CR}|{LR}", CRLR);
+ input = Regex.Replace(input, $"{CRLR}|{CR}|{LR}", replacement);
#endif
return input;
}
diff --git a/src/redmine-net-api/Http/Clients/HttpClient/HttpClientProvider.cs b/src/redmine-net-api/Http/Clients/HttpClient/HttpClientProvider.cs
new file mode 100644
index 00000000..3932402f
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/HttpClient/HttpClientProvider.cs
@@ -0,0 +1,257 @@
+#if !NET20
+using System;
+using System.Net.Http;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+using Redmine.Net.Api.Common;
+using Redmine.Net.Api.Options;
+
+namespace Redmine.Net.Api.Http.Clients.HttpClient;
+
+internal static class HttpClientProvider
+{
+ private static System.Net.Http.HttpClient _client;
+
+ ///
+ /// Gets an HttpClient instance. If an existing client is provided, it is returned; otherwise, a new one is created.
+ ///
+ public static System.Net.Http.HttpClient GetOrCreateHttpClient(System.Net.Http.HttpClient httpClient,
+ RedmineManagerOptions options)
+ {
+ if (_client != null)
+ {
+ return _client;
+ }
+
+ _client = httpClient ?? CreateClient(options);
+
+ return _client;
+ }
+
+ ///
+ /// Creates a new HttpClient instance configured with the specified options.
+ ///
+ private static System.Net.Http.HttpClient CreateClient(RedmineManagerOptions redmineManagerOptions)
+ {
+ ArgumentVerifier.ThrowIfNull(redmineManagerOptions, nameof(redmineManagerOptions));
+
+ var handler =
+ #if NET
+ CreateSocketHandler(redmineManagerOptions);
+ #elif NETFRAMEWORK
+ CreateHandler(redmineManagerOptions);
+ #endif
+
+ var client = new System.Net.Http.HttpClient(handler, disposeHandler: true);
+
+ if (redmineManagerOptions.BaseAddress != null)
+ {
+ client.BaseAddress = redmineManagerOptions.BaseAddress;
+ }
+
+ if (redmineManagerOptions.ApiClientOptions is not RedmineHttpClientOptions options)
+ {
+ return client;
+ }
+
+ if (options.Timeout.HasValue)
+ {
+ client.Timeout = options.Timeout.Value;
+ }
+
+ if (options.MaxResponseContentBufferSize.HasValue)
+ {
+ client.MaxResponseContentBufferSize = options.MaxResponseContentBufferSize.Value;
+ }
+
+#if NET5_0_OR_GREATER
+ if (options.DefaultRequestVersion != null)
+ {
+ client.DefaultRequestVersion = options.DefaultRequestVersion;
+ }
+
+ if (options.DefaultVersionPolicy != null)
+ {
+ client.DefaultVersionPolicy = options.DefaultVersionPolicy.Value;
+ }
+#endif
+
+ return client;
+ }
+
+#if NET
+ private static SocketsHttpHandler CreateSocketHandler(RedmineManagerOptions redmineManagerOptions)
+ {
+ var handler = new SocketsHttpHandler()
+ {
+ // Limit the lifetime of connections to better respect any DNS changes
+ PooledConnectionLifetime = TimeSpan.FromMinutes(2),
+
+ // Check cert revocation
+ SslOptions = new SslClientAuthenticationOptions()
+ {
+ CertificateRevocationCheckMode = X509RevocationMode.Online,
+ },
+ };
+
+ if (redmineManagerOptions.ApiClientOptions is not RedmineHttpClientOptions options)
+ {
+ return handler;
+ }
+
+ if (options.CookieContainer != null)
+ {
+ handler.CookieContainer = options.CookieContainer;
+ }
+
+ handler.Credentials = options.Credentials;
+ handler.Proxy = options.Proxy;
+
+ if (options.AutoRedirect.HasValue)
+ {
+ handler.AllowAutoRedirect = options.AutoRedirect.Value;
+ }
+
+ if (options.DecompressionFormat.HasValue)
+ {
+ handler.AutomaticDecompression = options.DecompressionFormat.Value;
+ }
+
+ if (options.PreAuthenticate.HasValue)
+ {
+ handler.PreAuthenticate = options.PreAuthenticate.Value;
+ }
+
+ if (options.UseCookies.HasValue)
+ {
+ handler.UseCookies = options.UseCookies.Value;
+ }
+
+ if (options.UseProxy.HasValue)
+ {
+ handler.UseProxy = options.UseProxy.Value;
+ }
+
+ if (options.MaxAutomaticRedirections.HasValue)
+ {
+ handler.MaxAutomaticRedirections = options.MaxAutomaticRedirections.Value;
+ }
+
+ handler.DefaultProxyCredentials = options.DefaultProxyCredentials;
+
+ if (options.MaxConnectionsPerServer.HasValue)
+ {
+ handler.MaxConnectionsPerServer = options.MaxConnectionsPerServer.Value;
+ }
+
+ if (options.MaxResponseHeadersLength.HasValue)
+ {
+ handler.MaxResponseHeadersLength = options.MaxResponseHeadersLength.Value;
+ }
+
+#if NET8_0_OR_GREATER
+ handler.MeterFactory = options.MeterFactory;
+#endif
+
+ return handler;
+ }
+#elif NETFRAMEWORK
+ private static HttpClientHandler CreateHandler(RedmineManagerOptions redmineManagerOptions)
+ {
+ var handler = new HttpClientHandler();
+ return ConfigureHandler(handler, redmineManagerOptions);
+ }
+
+ private static HttpClientHandler ConfigureHandler(HttpClientHandler handler, RedmineManagerOptions redmineManagerOptions)
+ {
+ if (redmineManagerOptions.ApiClientOptions is not RedmineHttpClientOptions options)
+ {
+ return handler;
+ }
+
+ if (options.UseDefaultCredentials.HasValue)
+ {
+ handler.UseDefaultCredentials = options.UseDefaultCredentials.Value;
+ }
+
+ if (options.CookieContainer != null)
+ {
+ handler.CookieContainer = options.CookieContainer;
+ }
+
+ if (handler.SupportsAutomaticDecompression && options.DecompressionFormat.HasValue)
+ {
+ handler.AutomaticDecompression = options.DecompressionFormat.Value;
+ }
+
+ if (handler.SupportsRedirectConfiguration)
+ {
+ if (options.AutoRedirect.HasValue)
+ {
+ handler.AllowAutoRedirect = options.AutoRedirect.Value;
+ }
+
+ if (options.MaxAutomaticRedirections.HasValue)
+ {
+ handler.MaxAutomaticRedirections = options.MaxAutomaticRedirections.Value;
+ }
+ }
+
+ if (options.ClientCertificateOptions != default)
+ {
+ handler.ClientCertificateOptions = options.ClientCertificateOptions;
+ }
+
+ handler.Credentials = options.Credentials;
+
+ if (options.UseProxy != null)
+ {
+ handler.UseProxy = options.UseProxy.Value;
+ if (handler.UseProxy && options.Proxy != null)
+ {
+ handler.Proxy = options.Proxy;
+ }
+ }
+
+ if (options.PreAuthenticate.HasValue)
+ {
+ handler.PreAuthenticate = options.PreAuthenticate.Value;
+ }
+
+ if (options.UseCookies.HasValue)
+ {
+ handler.UseCookies = options.UseCookies.Value;
+ }
+
+ if (options.MaxRequestContentBufferSize.HasValue)
+ {
+ handler.MaxRequestContentBufferSize = options.MaxRequestContentBufferSize.Value;
+ }
+
+#if NET471_OR_GREATER
+ handler.CheckCertificateRevocationList = options.CheckCertificateRevocationList;
+
+ if (options.DefaultProxyCredentials != null)
+ handler.DefaultProxyCredentials = options.DefaultProxyCredentials;
+
+ if (options.ServerCertificateCustomValidationCallback != null)
+ handler.ServerCertificateCustomValidationCallback = options.ServerCertificateCustomValidationCallback;
+
+ if (options.ServerCertificateValidationCallback != null)
+ handler.ServerCertificateCustomValidationCallback = options.ServerCertificateValidationCallback;
+
+ handler.SslProtocols = options.SslProtocols;
+
+ if (options.MaxConnectionsPerServer.HasValue)
+ handler.MaxConnectionsPerServer = options.MaxConnectionsPerServer.Value;
+
+ if (options.MaxResponseHeadersLength.HasValue)
+ handler.MaxResponseHeadersLength = options.MaxResponseHeadersLength.Value;
+#endif
+
+ return handler;
+ }
+#endif
+}
+
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Clients/HttpClient/HttpContentExtensions.cs b/src/redmine-net-api/Http/Clients/HttpClient/HttpContentExtensions.cs
new file mode 100644
index 00000000..c1b2515c
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/HttpClient/HttpContentExtensions.cs
@@ -0,0 +1,20 @@
+#if !NET20
+
+using System.Net;
+
+namespace Redmine.Net.Api.Http.Clients.HttpClient;
+
+internal static class HttpContentExtensions
+{
+ public static bool IsUnprocessableEntity(this HttpStatusCode statusCode)
+ {
+ return
+#if NET5_0_OR_GREATER
+ statusCode == HttpStatusCode.UnprocessableEntity;
+#else
+ (int)statusCode == 422;
+#endif
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Clients/HttpClient/HttpContentPolyfills.cs b/src/redmine-net-api/Http/Clients/HttpClient/HttpContentPolyfills.cs
new file mode 100644
index 00000000..ec5d7f4d
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/HttpClient/HttpContentPolyfills.cs
@@ -0,0 +1,35 @@
+
+#if !(NET20 || NET5_0_OR_GREATER)
+
+using System.IO;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Redmine.Net.Api.Http.Clients.HttpClient;
+
+internal static class HttpContentPolyfills
+{
+ internal static Task ReadAsStringAsync(this HttpContent httpContent, CancellationToken cancellationToken)
+ => httpContent.ReadAsStringAsync(
+#if !NETFRAMEWORK
+ cancellationToken
+#endif
+ );
+
+ internal static Task ReadAsStreamAsync(this HttpContent httpContent, CancellationToken cancellationToken)
+ => httpContent.ReadAsStreamAsync(
+#if !NETFRAMEWORK
+ cancellationToken
+#endif
+ );
+
+ internal static Task ReadAsByteArrayAsync(this HttpContent httpContent, CancellationToken cancellationToken)
+ => httpContent.ReadAsByteArrayAsync(
+#if !NETFRAMEWORK
+ cancellationToken
+#endif
+ );
+}
+
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Clients/HttpClient/HttpResponseHeadersExtensions.cs b/src/redmine-net-api/Http/Clients/HttpClient/HttpResponseHeadersExtensions.cs
new file mode 100644
index 00000000..37b44dcb
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/HttpClient/HttpResponseHeadersExtensions.cs
@@ -0,0 +1,22 @@
+#if !NET20
+using System.Collections.Specialized;
+using System.Net.Http.Headers;
+
+namespace Redmine.Net.Api.Http.Clients.HttpClient;
+
+internal static class HttpResponseHeadersExtensions
+{
+ public static NameValueCollection ToNameValueCollection(this HttpResponseHeaders headers)
+ {
+ if (headers == null) return null;
+
+ var collection = new NameValueCollection();
+ foreach (var header in headers)
+ {
+ var combinedValue = string.Join(", ", header.Value);
+ collection.Add(header.Key, combinedValue);
+ }
+ return collection;
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Clients/HttpClient/IRedmineHttpClientOptions.cs b/src/redmine-net-api/Http/Clients/HttpClient/IRedmineHttpClientOptions.cs
new file mode 100644
index 00000000..d8f6d9d2
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/HttpClient/IRedmineHttpClientOptions.cs
@@ -0,0 +1,88 @@
+#if NET40_OR_GREATER || NET
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Security;
+using System.Security.Authentication;
+using System.Security.Cryptography.X509Certificates;
+#if NET8_0_OR_GREATER
+using System.Diagnostics.Metrics;
+#endif
+
+
+namespace Redmine.Net.Api.Http.Clients.HttpClient;
+
+///
+///
+///
+public interface IRedmineHttpClientOptions : IRedmineApiClientOptions
+{
+ ///
+ ///
+ ///
+ ClientCertificateOption ClientCertificateOptions { get; set; }
+
+#if NET471_OR_GREATER || NET
+ ///
+ ///
+ ///
+ ICredentials DefaultProxyCredentials { get; set; }
+
+ ///
+ ///
+ ///
+ Func ServerCertificateCustomValidationCallback { get; set; }
+
+ ///
+ ///
+ ///
+ SslProtocols SslProtocols { get; set; }
+#endif
+
+ ///
+ ///
+ ///
+ public
+#if NET || NET471_OR_GREATER
+ Func
+#else
+ RemoteCertificateValidationCallback
+#endif
+ ServerCertificateValidationCallback { get; set; }
+
+#if NET8_0_OR_GREATER
+ ///
+ ///
+ ///
+ public IMeterFactory MeterFactory { get; set; }
+#endif
+
+ ///
+ ///
+ ///
+ bool SupportsAutomaticDecompression { get; set; }
+
+ ///
+ ///
+ ///
+ bool SupportsProxy { get; set; }
+
+ ///
+ ///
+ ///
+ bool SupportsRedirectConfiguration { get; set; }
+
+ ///
+ ///
+ ///
+ Version DefaultRequestVersion { get; set; }
+
+#if NET
+ ///
+ ///
+ ///
+ HttpVersionPolicy? DefaultVersionPolicy { get; set; }
+#endif
+}
+
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs
new file mode 100644
index 00000000..77186037
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs
@@ -0,0 +1,144 @@
+#if !NET20
+/*
+ Copyright 2011 - 2025 Adrian Popescu
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Redmine.Net.Api.Exceptions;
+using Redmine.Net.Api.Http.Helpers;
+using Redmine.Net.Api.Http.Messages;
+
+namespace Redmine.Net.Api.Http.Clients.HttpClient;
+
+internal sealed partial class InternalRedmineApiHttpClient
+{
+ protected override async Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null,
+ object content = null, IProgress progress = null, CancellationToken cancellationToken = default)
+ {
+ var httpMethod = GetHttpMethod(verb);
+ using (var requestMessage = CreateRequestMessage(address, httpMethod, requestOptions, content as HttpContent))
+ {
+ return await SendAsync(requestMessage, progress: progress, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task SendAsync(HttpRequestMessage requestMessage, IProgress progress = null, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ using (var httpResponseMessage = await _httpClient
+ .SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+ .ConfigureAwait(false))
+ {
+ if (httpResponseMessage.IsSuccessStatusCode)
+ {
+ if (httpResponseMessage.StatusCode == HttpStatusCode.NoContent)
+ {
+ return CreateApiResponseMessage(httpResponseMessage.Headers, HttpStatusCode.NoContent, []);
+ }
+
+ byte[] data;
+
+ if (requestMessage.Method == HttpMethod.Get && progress != null)
+ {
+ data = await DownloadWithProgressAsync(httpResponseMessage.Content, progress, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ data = await httpResponseMessage.Content.ReadAsByteArrayAsync(cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ return CreateApiResponseMessage(httpResponseMessage.Headers, httpResponseMessage.StatusCode, data);
+ }
+
+ var statusCode = (int)httpResponseMessage.StatusCode;
+ using (var stream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken)
+ .ConfigureAwait(false))
+ {
+ RedmineExceptionHelper.MapStatusCodeToException(statusCode, stream, null, Serializer);
+ }
+ }
+ }
+ catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested)
+ {
+ throw new RedmineApiException("Token has been cancelled", ex);
+ }
+ catch (OperationCanceledException ex) when (ex.InnerException is TimeoutException tex)
+ {
+ throw new RedmineApiException("Operation has timed out", ex);
+ }
+ catch (TaskCanceledException tcex) when (cancellationToken.IsCancellationRequested)
+ {
+ throw new RedmineApiException("Operation ahs been cancelled by user", tcex);
+ }
+ catch (TaskCanceledException tce)
+ {
+ throw new RedmineApiException(tce.Message, tce);
+ }
+ catch (HttpRequestException ex)
+ {
+ throw new RedmineApiException(ex.Message, ex);
+ }
+ catch (Exception ex) when (ex is not RedmineException)
+ {
+ throw new RedmineApiException(ex.Message, ex);
+ }
+
+ return null;
+ }
+
+ private static async Task DownloadWithProgressAsync(HttpContent httpContent, IProgress progress = null, CancellationToken cancellationToken = default)
+ {
+ var contentLength = httpContent.Headers.ContentLength ?? -1;
+ byte[] data;
+
+ if (contentLength > 0)
+ {
+ using (var stream = await httpContent.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false))
+ {
+ data = new byte[contentLength];
+ int bytesRead;
+ var totalBytesRead = 0;
+ var buffer = new byte[8192];
+
+ while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Buffer.BlockCopy(buffer, 0, data, totalBytesRead, bytesRead);
+ totalBytesRead += bytesRead;
+
+ var progressPercentage = (int)(totalBytesRead * 100 / contentLength);
+ progress?.Report(progressPercentage);
+ ReportProgress(progress, contentLength, totalBytesRead);
+ }
+ }
+ }
+ else
+ {
+ data = await httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
+ progress?.Report(100);
+ }
+
+ return data;
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs
new file mode 100644
index 00000000..a68897d6
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs
@@ -0,0 +1,170 @@
+#if !NET20
+/*
+ Copyright 2011 - 2024 Adrian Popescu
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using Redmine.Net.Api.Authentication;
+using Redmine.Net.Api.Extensions;
+using Redmine.Net.Api.Http.Constants;
+using Redmine.Net.Api.Http.Messages;
+using Redmine.Net.Api.Options;
+
+namespace Redmine.Net.Api.Http.Clients.HttpClient;
+
+internal sealed partial class InternalRedmineApiHttpClient : RedmineApiClient
+{
+ private static readonly HttpMethod PatchMethod = new HttpMethod("PATCH");
+ private static readonly HttpMethod DownloadMethod = new HttpMethod("DOWNLOAD");
+ private static readonly Encoding DefaultEncoding = Encoding.UTF8;
+
+ private readonly System.Net.Http.HttpClient _httpClient;
+
+ public InternalRedmineApiHttpClient(RedmineManagerOptions redmineManagerOptions)
+ : this(null, redmineManagerOptions)
+ {
+ _httpClient = HttpClientProvider.GetOrCreateHttpClient(null, redmineManagerOptions);
+ }
+
+ public InternalRedmineApiHttpClient(System.Net.Http.HttpClient httpClient,
+ RedmineManagerOptions redmineManagerOptions)
+ : base(redmineManagerOptions)
+ {
+ _httpClient = httpClient;
+ }
+
+ protected override object CreateContentFromPayload(string payload)
+ {
+ return new StringContent(payload, DefaultEncoding, Serializer.ContentType);
+ }
+
+ protected override object CreateContentFromBytes(byte[] data)
+ {
+ var content = new ByteArrayContent(data);
+ content.Headers.ContentType = new MediaTypeHeaderValue(RedmineConstants.CONTENT_TYPE_APPLICATION_STREAM);
+ return content;
+ }
+
+ protected override RedmineApiResponse HandleRequest(string address, string verb,
+ RequestOptions requestOptions = null,
+ object content = null, IProgress progress = null)
+ {
+ var httpMethod = GetHttpMethod(verb);
+ using (var requestMessage = CreateRequestMessage(address, httpMethod, requestOptions, content as HttpContent))
+ {
+ // LogRequest(verb, address, requestOptions);
+
+ var response = Send(requestMessage, progress);
+
+ // LogResponse(response.StatusCode);
+
+ return response;
+ }
+ }
+
+ private RedmineApiResponse Send(HttpRequestMessage requestMessage, IProgress progress = null)
+ {
+ return TaskExtensions.Synchronize(()=>SendAsync(requestMessage, progress));
+ }
+
+ private HttpRequestMessage CreateRequestMessage(string address, HttpMethod method,
+ RequestOptions requestOptions = null, HttpContent content = null)
+ {
+ var httpRequest = new HttpRequestMessage(method, address);
+
+ switch (Credentials)
+ {
+ case RedmineApiKeyAuthentication:
+ httpRequest.Headers.Add(RedmineConstants.API_KEY_AUTHORIZATION_HEADER_KEY, Credentials.Token);
+ break;
+ case RedmineBasicAuthentication:
+ httpRequest.Headers.Add(RedmineConstants.AUTHORIZATION_HEADER_KEY, Credentials.Token);
+ break;
+ }
+
+ if (requestOptions != null)
+ {
+ if (requestOptions.QueryString != null)
+ {
+ var uriToBeAppended = httpRequest.RequestUri.ToString();
+ var queryIndex = uriToBeAppended.IndexOf("?", StringComparison.Ordinal);
+ var hasQuery = queryIndex != -1;
+
+ var sb = new StringBuilder();
+ sb.Append('\\');
+ sb.Append(uriToBeAppended);
+ for (var index = 0; index < requestOptions.QueryString.Count; ++index)
+ {
+ var value = requestOptions.QueryString[index];
+
+ if (value == null)
+ {
+ continue;
+ }
+
+ var key = requestOptions.QueryString.Keys[index];
+
+ sb.Append(hasQuery ? '&' : '?');
+ sb.Append(Uri.EscapeDataString(key));
+ sb.Append('=');
+ sb.Append(Uri.EscapeDataString(value));
+ hasQuery = true;
+ }
+
+ var uriString = sb.ToString();
+
+ httpRequest.RequestUri = new Uri(uriString, UriKind.RelativeOrAbsolute);
+ }
+
+ if (!requestOptions.ImpersonateUser.IsNullOrWhiteSpace())
+ {
+ httpRequest.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, requestOptions.ImpersonateUser);
+ }
+ }
+
+ if (content != null)
+ {
+ httpRequest.Content = content ;
+ }
+
+ return httpRequest;
+ }
+
+ private static RedmineApiResponse CreateApiResponseMessage(HttpResponseHeaders headers, HttpStatusCode statusCode, byte[] content) => new RedmineApiResponse()
+ {
+ Content = content,
+ Headers = headers.ToNameValueCollection(),
+ StatusCode = statusCode,
+ };
+
+ private static HttpMethod GetHttpMethod(string verb)
+ {
+ return verb switch
+ {
+ HttpConstants.HttpVerbs.GET => HttpMethod.Get,
+ HttpConstants.HttpVerbs.POST => HttpMethod.Post,
+ HttpConstants.HttpVerbs.PUT => HttpMethod.Put,
+ HttpConstants.HttpVerbs.PATCH => PatchMethod,
+ HttpConstants.HttpVerbs.DELETE => HttpMethod.Delete,
+ HttpConstants.HttpVerbs.DOWNLOAD => HttpMethod.Get,
+ _ => throw new ArgumentException($"Unsupported HTTP verb: {verb}")
+ };
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Clients/HttpClient/RedmineHttpClientOptions.cs b/src/redmine-net-api/Http/Clients/HttpClient/RedmineHttpClientOptions.cs
new file mode 100644
index 00000000..dc8e0b6a
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/HttpClient/RedmineHttpClientOptions.cs
@@ -0,0 +1,75 @@
+#if !NET20
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Security;
+using System.Security.Authentication;
+using System.Security.Cryptography.X509Certificates;
+#if NET8_0_OR_GREATER
+using System.Diagnostics.Metrics;
+#endif
+
+namespace Redmine.Net.Api.Http.Clients.HttpClient;
+
+///
+///
+///
+public sealed class RedmineHttpClientOptions: RedmineApiClientOptions
+{
+#if NET8_0_OR_GREATER
+ ///
+ ///
+ ///
+ public IMeterFactory MeterFactory { get; set; }
+#endif
+
+ ///
+ ///
+ ///
+ public Version DefaultRequestVersion { get; set; }
+
+#if NET
+ ///
+ ///
+ ///
+ public HttpVersionPolicy? DefaultVersionPolicy { get; set; }
+#endif
+ ///
+ ///
+ ///
+ public ICredentials DefaultProxyCredentials { get; set; }
+
+ ///
+ ///
+ ///
+ public ClientCertificateOption ClientCertificateOptions { get; set; }
+
+
+
+#if NETFRAMEWORK
+ ///
+ ///
+ ///
+ public Func ServerCertificateCustomValidationCallback
+ {
+ get;
+ set;
+ }
+
+ ///
+ ///
+ ///
+ public SslProtocols SslProtocols { get; set; }
+ #endif
+ ///
+ ///
+ ///
+ public
+#if NET || NET471_OR_GREATER
+ Func
+#else
+ RemoteCertificateValidationCallback
+#endif
+ ServerCertificateValidationCallback { get; set; }
+}
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs
new file mode 100644
index 00000000..a2678963
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs
@@ -0,0 +1,129 @@
+/*
+ Copyright 2011 - 2025 Adrian Popescu
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+#if!(NET20)
+using System;
+using System.Collections.Specialized;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Redmine.Net.Api.Exceptions;
+using Redmine.Net.Api.Http.Messages;
+
+namespace Redmine.Net.Api.Http.Clients.WebClient
+{
+ ///
+ ///
+ ///
+ internal sealed partial class InternalRedmineApiWebClient
+ {
+ protected override async Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, object content = null,
+ IProgress progress = null, CancellationToken cancellationToken = default)
+ {
+ return await SendAsync(CreateRequestMessage(address, verb, requestOptions, content as RedmineApiRequestContent), progress, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task SendAsync(RedmineApiRequest requestMessage, IProgress progress = null, CancellationToken cancellationToken = default)
+ {
+ System.Net.WebClient webClient = null;
+ byte[] response = null;
+ HttpStatusCode? statusCode = null;
+ NameValueCollection responseHeaders = null;
+ CancellationTokenRegistration cancellationTokenRegistration = default;
+
+ try
+ {
+ webClient = _webClientFunc();
+ cancellationTokenRegistration =
+ cancellationToken.Register(
+ static state => ((System.Net.WebClient)state).CancelAsync(),
+ webClient
+ );
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (progress != null)
+ {
+ webClient.DownloadProgressChanged += (_, e) =>
+ {
+ progress.Report(e.ProgressPercentage);
+ };
+ }
+
+ SetWebClientHeaders(webClient, requestMessage);
+
+ if(IsGetOrDownload(requestMessage.Method))
+ {
+ response = await webClient.DownloadDataTaskAsync(requestMessage.RequestUri)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ byte[] payload;
+ if (requestMessage.Content != null)
+ {
+ webClient.Headers.Add(HttpRequestHeader.ContentType, requestMessage.Content.ContentType);
+ payload = requestMessage.Content.Body;
+ }
+ else
+ {
+ payload = EmptyBytes;
+ }
+
+ response = await webClient.UploadDataTaskAsync(requestMessage.RequestUri, requestMessage.Method, payload)
+ .ConfigureAwait(false);
+ }
+
+ responseHeaders = webClient.ResponseHeaders;
+ if (webClient is InternalWebClient iwc)
+ {
+ statusCode = iwc.StatusCode;
+ }
+ }
+ catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ throw new RedmineApiException("The operation was canceled by the user.", ex);
+ }
+ }
+ catch (WebException webException)
+ {
+ HandleWebException(webException, Serializer);
+ }
+ finally
+ {
+ #if NETFRAMEWORK
+ cancellationTokenRegistration.Dispose();
+ #else
+ await cancellationTokenRegistration.DisposeAsync().ConfigureAwait(false);
+ #endif
+
+ webClient?.Dispose();
+ }
+
+ return new RedmineApiResponse()
+ {
+ Headers = responseHeaders,
+ Content = response,
+ StatusCode = statusCode ?? HttpStatusCode.OK,
+ };
+ }
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs
new file mode 100644
index 00000000..f08e3474
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs
@@ -0,0 +1,282 @@
+/*
+ Copyright 2011 - 2025 Adrian Popescu
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+using System;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+using Redmine.Net.Api.Authentication;
+using Redmine.Net.Api.Exceptions;
+using Redmine.Net.Api.Extensions;
+using Redmine.Net.Api.Http.Constants;
+using Redmine.Net.Api.Http.Extensions;
+using Redmine.Net.Api.Http.Helpers;
+using Redmine.Net.Api.Http.Messages;
+using Redmine.Net.Api.Logging;
+using Redmine.Net.Api.Options;
+using Redmine.Net.Api.Serialization;
+
+namespace Redmine.Net.Api.Http.Clients.WebClient
+{
+ ///
+ ///
+ ///
+ internal sealed partial class InternalRedmineApiWebClient : RedmineApiClient
+ {
+ private static readonly byte[] EmptyBytes = Encoding.UTF8.GetBytes(string.Empty);
+ private readonly Func _webClientFunc;
+
+ public InternalRedmineApiWebClient(RedmineManagerOptions redmineManagerOptions)
+ : this(() => new InternalWebClient(redmineManagerOptions), redmineManagerOptions)
+ {
+ }
+
+ public InternalRedmineApiWebClient(Func webClientFunc, RedmineManagerOptions redmineManagerOptions)
+ : base(redmineManagerOptions)
+ {
+ _webClientFunc = webClientFunc;
+ }
+
+ protected override object CreateContentFromPayload(string payload)
+ {
+ return RedmineApiRequestContent.CreateString(payload, Serializer.ContentType);
+ }
+
+ protected override object CreateContentFromBytes(byte[] data)
+ {
+ return RedmineApiRequestContent.CreateBinary(data);
+ }
+
+ protected override RedmineApiResponse HandleRequest(string address, string verb, RequestOptions requestOptions = null, object content = null, IProgress progress = null)
+ {
+ var requestMessage = CreateRequestMessage(address, verb, requestOptions, content as RedmineApiRequestContent);
+
+ if (Options.LoggingOptions?.IncludeHttpDetails == true)
+ {
+ Options.Logger.Debug($"Request HTTP {verb} {address}");
+
+ if (requestOptions?.QueryString != null)
+ {
+ Options.Logger.Debug($"Query parameters: {requestOptions.QueryString.ToQueryString()}");
+ }
+ }
+
+ var responseMessage = Send(requestMessage, progress);
+
+ if (Options.LoggingOptions?.IncludeHttpDetails == true)
+ {
+ Options.Logger.Debug($"Response status: {responseMessage.StatusCode}");
+ }
+
+ return responseMessage;
+ }
+
+ private static RedmineApiRequest CreateRequestMessage(string address, string verb, RequestOptions requestOptions = null, RedmineApiRequestContent content = null)
+ {
+ var req = new RedmineApiRequest()
+ {
+ RequestUri = address,
+ Method = verb,
+ };
+
+ if (requestOptions != null)
+ {
+ req.QueryString = requestOptions.QueryString;
+ req.ImpersonateUser = requestOptions.ImpersonateUser;
+ }
+
+ if (content != null)
+ {
+ req.Content = content;
+ }
+
+ return req;
+ }
+
+ private RedmineApiResponse Send(RedmineApiRequest requestMessage, IProgress progress = null)
+ {
+ System.Net.WebClient webClient = null;
+ byte[] response = null;
+ HttpStatusCode? statusCode = null;
+ NameValueCollection responseHeaders = null;
+
+ try
+ {
+ webClient = _webClientFunc();
+
+ SetWebClientHeaders(webClient, requestMessage);
+
+ if (IsGetOrDownload(requestMessage.Method))
+ {
+ response = requestMessage.Method == HttpConstants.HttpVerbs.DOWNLOAD
+ ? DownloadWithProgress(requestMessage.RequestUri, webClient, progress)
+ : webClient.DownloadData(requestMessage.RequestUri);
+ }
+ else
+ {
+ byte[] payload;
+ if (requestMessage.Content != null)
+ {
+ webClient.Headers.Add(HttpRequestHeader.ContentType, requestMessage.Content.ContentType);
+ payload = requestMessage.Content.Body;
+ }
+ else
+ {
+ payload = EmptyBytes;
+ }
+
+ response = webClient.UploadData(requestMessage.RequestUri, requestMessage.Method, payload);
+ }
+
+ responseHeaders = webClient.ResponseHeaders;
+ if (webClient is InternalWebClient iwc)
+ {
+ statusCode = iwc.StatusCode;
+ }
+ }
+ catch (WebException webException)
+ {
+ HandleWebException(webException, Serializer);
+ }
+ finally
+ {
+ webClient?.Dispose();
+ }
+
+ return new RedmineApiResponse()
+ {
+ Headers = responseHeaders,
+ Content = response,
+ StatusCode = statusCode ?? HttpStatusCode.OK,
+ };
+ }
+
+ private void SetWebClientHeaders(System.Net.WebClient webClient, RedmineApiRequest requestMessage)
+ {
+ if (requestMessage.QueryString != null)
+ {
+ webClient.QueryString = requestMessage.QueryString;
+ }
+
+ switch (Credentials)
+ {
+ case RedmineApiKeyAuthentication:
+ webClient.Headers.Add(RedmineConstants.API_KEY_AUTHORIZATION_HEADER_KEY,Credentials.Token);
+ break;
+ case RedmineBasicAuthentication:
+ webClient.Headers.Add(RedmineConstants.AUTHORIZATION_HEADER_KEY, Credentials.Token);
+ break;
+ }
+
+ if (!requestMessage.ImpersonateUser.IsNullOrWhiteSpace())
+ {
+ webClient.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, requestMessage.ImpersonateUser);
+ }
+ }
+
+ private static byte[] DownloadWithProgress(string url, System.Net.WebClient webClient, IProgress progress)
+ {
+ var contentLength = GetContentLength(webClient);
+ byte[] data;
+ if (contentLength > 0)
+ {
+ using (var respStream = webClient.OpenRead(url))
+ {
+ data = new byte[contentLength];
+ var buffer = new byte[4096];
+ int bytesRead;
+ var totalBytesRead = 0;
+
+ while ((bytesRead = respStream.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ Buffer.BlockCopy(buffer, 0, data, totalBytesRead, bytesRead);
+ totalBytesRead += bytesRead;
+
+ ReportProgress(progress, contentLength, totalBytesRead);
+ }
+ }
+ }
+ else
+ {
+ data = webClient.DownloadData(url);
+ progress?.Report(100);
+ }
+
+ return data;
+ }
+
+ private static int GetContentLength(System.Net.WebClient webClient)
+ {
+ var total = -1;
+ if (webClient.ResponseHeaders == null)
+ {
+ return total;
+ }
+
+ var contentLengthAsString = webClient.ResponseHeaders[HttpRequestHeader.ContentLength];
+ total = Convert.ToInt32(contentLengthAsString, CultureInfo.InvariantCulture);
+
+ return total;
+ }
+
+ ///
+ /// Handles the web exception.
+ ///
+ /// The exception.
+ ///
+ /// Timeout!
+ /// Bad domain name!
+ ///
+ ///
+ ///
+ ///
+ /// The page that you are trying to update is staled!
+ ///
+ ///
+ public static void HandleWebException(WebException exception, IRedmineSerializer serializer)
+ {
+ if (exception == null)
+ {
+ return;
+ }
+
+ var innerException = exception.InnerException ?? exception;
+
+ switch (exception.Status)
+ {
+ case WebExceptionStatus.Timeout:
+ throw new RedmineTimeoutException(nameof(WebExceptionStatus.Timeout), innerException);
+ case WebExceptionStatus.NameResolutionFailure:
+ throw new NameResolutionFailureException("Bad domain name.", innerException);
+ case WebExceptionStatus.ProtocolError:
+ if (exception.Response != null)
+ {
+ var statusCode = exception.Response is HttpWebResponse httpResponse
+ ? (int)httpResponse.StatusCode
+ : (int)HttpStatusCode.InternalServerError;
+
+ using var responseStream = exception.Response.GetResponseStream();
+ RedmineExceptionHelper.MapStatusCodeToException(statusCode, responseStream, innerException, serializer);
+ }
+
+ break;
+ }
+ throw new RedmineException(exception.Message, innerException);
+ }
+ }
+}
diff --git a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalWebClient.cs
similarity index 79%
rename from src/redmine-net-api/Net/WebClient/InternalWebClient.cs
rename to src/redmine-net-api/Http/Clients/WebClient/InternalWebClient.cs
index 2bec6d92..fabd7219 100644
--- a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs
+++ b/src/redmine-net-api/Http/Clients/WebClient/InternalWebClient.cs
@@ -13,16 +13,17 @@ You may obtain a copy of the License at
See the License for the specific language governing permissions and
limitations under the License.
*/
+
using System;
using System.Net;
using Redmine.Net.Api.Exceptions;
-using Redmine.Net.Api.Extensions;
+using Redmine.Net.Api.Options;
-namespace Redmine.Net.Api.Net.WebClient;
+namespace Redmine.Net.Api.Http.Clients.WebClient;
internal sealed class InternalWebClient : System.Net.WebClient
{
- private readonly IRedmineWebClientOptions _webClientOptions;
+ private readonly RedmineWebClientOptions _webClientOptions;
#pragma warning disable SYSLIB0014
public InternalWebClient(RedmineManagerOptions redmineManagerOptions)
@@ -43,9 +44,9 @@ protected override WebRequest GetWebRequest(Uri address)
return base.GetWebRequest(address);
}
- httpWebRequest.UserAgent = _webClientOptions.UserAgent.ValueOrFallback("RedmineDotNetAPIClient");
+ httpWebRequest.UserAgent = _webClientOptions.UserAgent;
- httpWebRequest.AutomaticDecompression = _webClientOptions.DecompressionFormat ?? DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.None;
+ AssignIfHasValue(_webClientOptions.DecompressionFormat, value => httpWebRequest.AutomaticDecompression = value);
AssignIfHasValue(_webClientOptions.AutoRedirect, value => httpWebRequest.AllowAutoRedirect = value);
@@ -78,14 +79,14 @@ protected override WebRequest GetWebRequest(Uri address)
httpWebRequest.Credentials = _webClientOptions.Credentials;
- #if NET40_OR_GREATER || NETCOREAPP
+ #if NET40_OR_GREATER || NET
if (_webClientOptions.ClientCertificates != null)
{
httpWebRequest.ClientCertificates = _webClientOptions.ClientCertificates;
}
#endif
- #if (NET45_OR_GREATER || NETCOREAPP)
+ #if (NET45_OR_GREATER || NET)
httpWebRequest.ServerCertificateValidationCallback = _webClientOptions.ServerCertificateValidationCallback;
#endif
@@ -101,6 +102,28 @@ protected override WebRequest GetWebRequest(Uri address)
throw new RedmineException(webException.GetBaseException().Message, webException);
}
}
+
+ public HttpStatusCode StatusCode { get; private set; }
+
+ protected override WebResponse GetWebResponse(WebRequest request)
+ {
+ var response = base.GetWebResponse(request);
+ if (response is HttpWebResponse httpResponse)
+ {
+ StatusCode = httpResponse.StatusCode;
+ }
+ return response;
+ }
+
+ protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
+ {
+ var response = base.GetWebResponse(request, result);
+ if (response is HttpWebResponse httpResponse)
+ {
+ StatusCode = httpResponse.StatusCode;
+ }
+ return response;
+ }
private static void AssignIfHasValue(T? nullableValue, Action assignAction) where T : struct
{
diff --git a/src/redmine-net-api/Http/Clients/WebClient/RedmineApiRequestContent.cs b/src/redmine-net-api/Http/Clients/WebClient/RedmineApiRequestContent.cs
new file mode 100644
index 00000000..9d9c33f0
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/WebClient/RedmineApiRequestContent.cs
@@ -0,0 +1,93 @@
+/*
+ Copyright 2011 - 2025 Adrian Popescu
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+using System;
+using System.Text;
+using Redmine.Net.Api.Http.Constants;
+
+namespace Redmine.Net.Api.Http.Clients.WebClient;
+
+internal class RedmineApiRequestContent : IDisposable
+{
+ private static readonly byte[] _emptyByteArray = [];
+ private bool _isDisposed;
+
+ ///
+ /// Gets the content type of the request.
+ ///
+ public string ContentType { get; }
+
+ ///
+ /// Gets the body of the request.
+ ///
+ public byte[] Body { get; }
+
+ ///
+ /// Gets the length of the request body.
+ ///
+ public int Length => Body?.Length ?? 0;
+
+ ///
+ /// Creates a new instance of RedmineApiRequestContent.
+ ///
+ /// The content type of the request.
+ /// The body of the request.
+ /// Thrown when the contentType is null.
+ public RedmineApiRequestContent(string contentType, byte[] body)
+ {
+ ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType));
+ Body = body ?? _emptyByteArray;
+ }
+
+ ///
+ /// Creates a text-based request content with the specified MIME type.
+ ///
+ /// The text content.
+ /// The MIME type of the content.
+ /// The encoding to use (defaults to UTF-8).
+ /// A new RedmineApiRequestContent instance.
+ public static RedmineApiRequestContent CreateString(string text, string mimeType, Encoding encoding = null)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return new RedmineApiRequestContent(mimeType, _emptyByteArray);
+ }
+
+ encoding ??= Encoding.UTF8;
+ return new RedmineApiRequestContent(mimeType, encoding.GetBytes(text));
+ }
+
+ ///
+ /// Creates a binary request content.
+ ///
+ /// The binary data.
+ /// A new RedmineApiRequestContent instance.
+ public static RedmineApiRequestContent CreateBinary(byte[] data)
+ {
+ return new RedmineApiRequestContent(HttpConstants.ContentTypes.ApplicationOctetStream, data);
+ }
+
+ ///
+ /// Disposes the resources used by this instance.
+ ///
+ public void Dispose()
+ {
+ if (!_isDisposed)
+ {
+ _isDisposed = true;
+ }
+ }
+}
diff --git a/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs b/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs
new file mode 100644
index 00000000..013d5a82
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs
@@ -0,0 +1,83 @@
+/*
+ Copyright 2011 - 2025 Adrian Popescu
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+using System.Net;
+#if (NET45_OR_GREATER || NET)
+using System.Net.Security;
+#endif
+using System.Security.Cryptography.X509Certificates;
+
+namespace Redmine.Net.Api.Http.Clients.WebClient;
+///
+///
+///
+public sealed class RedmineWebClientOptions: RedmineApiClientOptions
+{
+
+ ///
+ ///
+ ///
+ public bool? KeepAlive { get; set; }
+
+ ///
+ ///
+ ///
+ public bool? UnsafeAuthenticatedConnectionSharing { get; set; }
+
+ ///
+ ///
+ ///
+ public int? DefaultConnectionLimit { get; set; }
+
+ ///
+ ///
+ ///
+ public int? DnsRefreshTimeout { get; set; }
+
+ ///
+ ///
+ ///
+ public bool? EnableDnsRoundRobin { get; set; }
+
+ ///
+ ///
+ ///
+ public int? MaxServicePoints { get; set; }
+
+ ///
+ ///
+ ///
+ public int? MaxServicePointIdleTime { get; set; }
+
+ #if(NET46_OR_GREATER || NET)
+ ///
+ ///
+ ///
+ public bool? ReusePort { get; set; }
+ #endif
+
+ ///
+ ///
+ ///
+ public SecurityProtocolType? SecurityProtocolType { get; set; }
+
+#if (NET45_OR_GREATER || NET)
+ ///
+ ///
+ ///
+ public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; }
+ #endif
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs b/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs
new file mode 100644
index 00000000..ceb2a2cc
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs
@@ -0,0 +1,32 @@
+using Redmine.Net.Api.Extensions;
+using Redmine.Net.Api.Serialization;
+
+namespace Redmine.Net.Api.Http.Clients.WebClient;
+
+internal static class WebClientExtensions
+{
+ public static void ApplyHeaders(this System.Net.WebClient client, RequestOptions options, IRedmineSerializer serializer)
+ {
+ client.Headers.Add(RedmineConstants.CONTENT_TYPE_HEADER_KEY, options.ContentType ?? serializer.ContentType);
+
+ if (!options.UserAgent.IsNullOrWhiteSpace())
+ {
+ client.Headers.Add(RedmineConstants.USER_AGENT_HEADER_KEY, options.UserAgent);
+ }
+
+ if (!options.ImpersonateUser.IsNullOrWhiteSpace())
+ {
+ client.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, options.ImpersonateUser);
+ }
+
+ if (options.Headers is not { Count: > 0 })
+ {
+ return;
+ }
+
+ foreach (var header in options.Headers)
+ {
+ client.Headers.Add(header.Key, header.Value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs b/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs
new file mode 100644
index 00000000..ff8c664b
--- /dev/null
+++ b/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Net;
+using System.Text;
+using Redmine.Net.Api.Options;
+
+namespace Redmine.Net.Api.Http.Clients.WebClient;
+
+internal static class WebClientProvider
+{
+ ///
+ /// Creates a new WebClient instance with the specified options.
+ ///
+ /// The options for the Redmine manager.
+ /// A new WebClient instance.
+ public static System.Net.WebClient CreateWebClient(RedmineManagerOptions options)
+ {
+ var webClient = new InternalWebClient(options);
+
+ if (options?.ApiClientOptions is RedmineWebClientOptions webClientOptions)
+ {
+ ConfigureWebClient(webClient, webClientOptions);
+ }
+
+ return webClient;
+ }
+
+ ///
+ /// Configures a WebClient instance with the specified options.
+ ///
+ /// The WebClient instance to configure.
+ /// The options to apply.
+ private static void ConfigureWebClient(System.Net.WebClient webClient, RedmineWebClientOptions options)
+ {
+ if (options == null) return;
+
+ webClient.Proxy = options.Proxy;
+ webClient.Headers = null;
+ webClient.BaseAddress = null;
+ webClient.CachePolicy = null;
+ webClient.Credentials = null;
+ webClient.Encoding = Encoding.UTF8;
+ webClient.UseDefaultCredentials = false;
+
+ // if (options.Timeout.HasValue && options.Timeout.Value != TimeSpan.Zero)
+ // {
+ // webClient.Timeout = options.Timeout;
+ // }
+ //
+ // if (options.KeepAlive.HasValue)
+ // {
+ // webClient.KeepAlive = options.KeepAlive.Value;
+ // }
+ //
+ // if (options.UnsafeAuthenticatedConnectionSharing.HasValue)
+ // {
+ // webClient.UnsafeAuthenticatedConnectionSharing = options.UnsafeAuthenticatedConnectionSharing.Value;
+ // }
+ //
+ // #if NET40_OR_GREATER || NET
+ // if (options.ClientCertificates != null)
+ // {
+ // webClient.ClientCertificates = options.ClientCertificates;
+ // }
+ // #endif
+
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Constants/HttpConstants.cs b/src/redmine-net-api/Http/Constants/HttpConstants.cs
new file mode 100644
index 00000000..d1666c4b
--- /dev/null
+++ b/src/redmine-net-api/Http/Constants/HttpConstants.cs
@@ -0,0 +1,106 @@
+namespace Redmine.Net.Api.Http.Constants;
+
+///
+///
+///
+public static class HttpConstants
+{
+ ///
+ /// HTTP status codes including custom codes used by Redmine.
+ ///
+ internal static class StatusCodes
+ {
+ public const int Unauthorized = 401;
+ public const int Forbidden = 403;
+ public const int NotFound = 404;
+ public const int NotAcceptable = 406;
+ public const int RequestTimeout = 408;
+ public const int Conflict = 409;
+ public const int UnprocessableEntity = 422;
+ public const int TooManyRequests = 429;
+ public const int InternalServerError = 500;
+ public const int BadGateway = 502;
+ public const int ServiceUnavailable = 503;
+ public const int GatewayTimeout = 504;
+ }
+
+ ///
+ /// Standard HTTP headers used in API requests and responses.
+ ///
+ internal static class Headers
+ {
+ public const string Authorization = "Authorization";
+ public const string ApiKey = "X-Redmine-API-Key";
+ public const string Impersonate = "X-Redmine-Switch-User";
+ public const string ContentType = "Content-Type";
+ }
+
+ internal static class Names
+ {
+ /// HTTP User-Agent header name.
+ public static string UserAgent => "User-Agent";
+ }
+
+ internal static class Values
+ {
+ /// User agent string to use for all HTTP requests.
+ public static string UserAgent => "Redmine-NET-API";
+ }
+
+ ///
+ /// MIME content types used in API requests and responses.
+ ///
+ internal static class ContentTypes
+ {
+ public const string ApplicationJson = "application/json";
+ public const string ApplicationXml = "application/xml";
+ public const string ApplicationOctetStream = "application/octet-stream";
+ }
+
+ ///
+ /// Error messages for different HTTP status codes.
+ ///
+ internal static class ErrorMessages
+ {
+ public const string NotFound = "The requested resource was not found.";
+ public const string Unauthorized = "Authentication is required or has failed.";
+ public const string Forbidden = "You don't have permission to access this resource.";
+ public const string Conflict = "The resource you are trying to update has been modified since you last retrieved it.";
+ public const string NotAcceptable = "The requested format is not supported.";
+ public const string InternalServerError = "The server encountered an unexpected error.";
+ public const string UnprocessableEntity = "Validation failed for the submitted data.";
+ public const string Cancelled = "The operation was cancelled.";
+ public const string TimedOut = "The operation has timed out.";
+ }
+
+ ///
+ ///
+ ///
+ internal static class HttpVerbs
+ {
+ ///
+ /// Represents an HTTP GET protocol method that is used to get an entity identified by a URI.
+ ///
+ public const string GET = "GET";
+ ///
+ /// Represents an HTTP PUT protocol method that is used to replace an entity identified by a URI.
+ ///
+ public const string PUT = "PUT";
+ ///
+ /// Represents an HTTP POST protocol method that is used to post a new entity as an addition to a URI.
+ ///
+ public const string POST = "POST";
+ ///
+ /// Represents an HTTP PATCH protocol method that is used to patch an existing entity identified by a URI.
+ ///
+ public const string PATCH = "PATCH";
+ ///
+ /// Represents an HTTP DELETE protocol method that is used to delete an existing entity identified by a URI.
+ ///
+ public const string DELETE = "DELETE";
+
+ internal const string DOWNLOAD = "DOWNLOAD";
+
+ internal const string UPLOAD = "UPLOAD";
+ }
+}
diff --git a/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs
new file mode 100644
index 00000000..3cf95a1d
--- /dev/null
+++ b/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs
@@ -0,0 +1,268 @@
+/*
+ Copyright 2011 - 2025 Adrian Popescu
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Text;
+using Redmine.Net.Api.Extensions;
+
+namespace Redmine.Net.Api.Http.Extensions
+{
+ ///
+ ///
+ ///
+ public static class NameValueCollectionExtensions
+ {
+ ///
+ /// Gets the parameter value.
+ ///
+ /// The parameters.
+ /// Name of the parameter.
+ ///
+ public static string GetParameterValue(this NameValueCollection parameters, string parameterName)
+ {
+ return GetValue(parameters, parameterName);
+ }
+
+ ///
+ /// Gets the parameter value.
+ ///
+ /// The parameters.
+ /// Name of the parameter.
+ ///
+ public static string GetValue(this NameValueCollection parameters, string key)
+ {
+ if (parameters == null)
+ {
+ return null;
+ }
+
+ var value = parameters.Get(key);
+
+ return value.IsNullOrWhiteSpace() ? null : value;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static string ToQueryString(this NameValueCollection requestParameters)
+ {
+ if (requestParameters == null || requestParameters.Count == 0)
+ {
+ return null;
+ }
+
+ var delimiter = string.Empty;
+
+ var stringBuilder = new StringBuilder();
+
+ for (var index = 0; index < requestParameters.Count; ++index)
+ {
+ stringBuilder
+ .Append(delimiter)
+ .Append(requestParameters.AllKeys[index].ToString(CultureInfo.InvariantCulture))
+ .Append('=')
+ .Append(requestParameters[index].ToString(CultureInfo.InvariantCulture));
+ delimiter = "&";
+ }
+
+ var queryString = stringBuilder.ToString();
+
+ stringBuilder.Length = 0;
+
+ return queryString;
+ }
+
+ internal static NameValueCollection AddPagingParameters(this NameValueCollection parameters, int pageSize, int offset)
+ {
+ parameters ??= new NameValueCollection();
+
+ if(pageSize <= 0)
+ {
+ pageSize = RedmineConstants.DEFAULT_PAGE_SIZE_VALUE;
+ }
+
+ if(offset < 0)
+ {
+ offset = 0;
+ }
+
+ parameters.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString());
+ parameters.Set(RedmineKeys.OFFSET, offset.ToInvariantString());
+
+ return parameters;
+ }
+
+ internal static NameValueCollection AddParamsIfExist(this NameValueCollection parameters, string[] include)
+ {
+ if (include is not {Length: > 0})
+ {
+ return parameters;
+ }
+
+ parameters ??= new NameValueCollection();
+
+ parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include));
+
+ return parameters;
+ }
+
+ internal static void AddIfNotNull(this NameValueCollection nameValueCollection, string key, string value)
+ {
+ if (!value.IsNullOrWhiteSpace())
+ {
+ nameValueCollection.Add(key, value);
+ }
+ }
+
+ internal static void AddIfNotNull(this NameValueCollection nameValueCollection, string key, bool? value)
+ {
+ if (value.HasValue)
+ {
+ nameValueCollection.Add(key, value.Value.ToInvariantString());
+ }
+ }
+
+ ///
+ /// Creates a new NameValueCollection with an initial key-value pair.
+ ///
+ /// The key for the first item.
+ /// The value for the first item.
+ /// A new NameValueCollection containing the specified key-value pair.
+ public static NameValueCollection WithItem(this string key, string value)
+ {
+ var collection = new NameValueCollection();
+ collection.Add(key, value);
+ return collection;
+ }
+
+ ///
+ /// Adds a new key-value pair to an existing NameValueCollection and returns the collection for chaining.
+ ///
+ /// The NameValueCollection to add to.
+ /// The key to add.
+ /// The value to add.
+ /// The NameValueCollection with the new key-value pair added.
+ public static NameValueCollection AndItem(this NameValueCollection collection, string key, string value)
+ {
+ collection.Add(key, value);
+ return collection;
+ }
+
+ ///
+ /// Adds a new key-value pair to an existing NameValueCollection if the condition is true.
+ ///
+ /// The NameValueCollection to add to.
+ /// The condition to evaluate.
+ /// The key to add if condition is true.
+ /// The value to add if condition is true.
+ /// The NameValueCollection, potentially with a new key-value pair added.
+ public static NameValueCollection AndItemIf(this NameValueCollection collection, bool condition, string key, string value)
+ {
+ if (condition)
+ {
+ collection.Add(key, value);
+ }
+ return collection;
+ }
+
+ ///
+ /// Adds a new key-value pair to an existing NameValueCollection if the value is not null.
+ ///
+ /// The NameValueCollection to add to.
+ /// The key to add if value is not null.
+ /// The value to check and add.
+ /// The NameValueCollection, potentially with a new key-value pair added.
+ public static NameValueCollection AndItemIfNotNull(this NameValueCollection collection, string key, string value)
+ {
+ if (value != null)
+ {
+ collection.Add(key, value);
+ }
+ return collection;
+ }
+
+ ///
+ /// Creates a new NameValueCollection with an initial key-value pair where the value is converted from an integer.
+ ///
+ /// The key for the first item.
+ /// The integer value to be converted to string.
+ /// A new NameValueCollection containing the specified key-value pair.
+ public static NameValueCollection WithInt(this string key, int value)
+ {
+ return key.WithItem(value.ToInvariantString());
+ }
+
+ ///
+ /// Adds a new key-value pair to an existing NameValueCollection where the value is converted from an integer.
+ ///
+ /// The NameValueCollection to add to.
+ /// The key to add.
+ /// The integer value to be converted to string.
+ /// The NameValueCollection with the new key-value pair added.
+ public static NameValueCollection AndInt(this NameValueCollection collection, string key, int value)
+ {
+ return collection.AndItem(key, value.ToInvariantString());
+ }
+
+
+ ///
+ /// Converts a NameValueCollection to a Dictionary.
+ ///
+ /// The collection to convert.
+ /// A new Dictionary containing the collection's key-value pairs.
+ public static Dictionary ToDictionary(this NameValueCollection collection)
+ {
+ var dict = new Dictionary();
+
+ if (collection != null)
+ {
+ foreach (string key in collection.Keys)
+ {
+ dict[key] = collection[key];
+ }
+ }
+
+ return dict;
+ }
+
+ ///
+ /// Creates a new NameValueCollection from a dictionary of key-value pairs.
+ ///
+ /// Dictionary of key-value pairs to add to the collection.
+ /// A new NameValueCollection containing the specified key-value pairs.
+ public static NameValueCollection ToNameValueCollection(this Dictionary keyValuePairs)
+ {
+ var collection = new NameValueCollection();
+
+ if (keyValuePairs != null)
+ {
+ foreach (var pair in keyValuePairs)
+ {
+ collection.Add(pair.Key, pair.Value);
+ }
+ }
+
+ return collection;
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Extensions/RedmineApiResponseExtensions.cs b/src/redmine-net-api/Http/Extensions/RedmineApiResponseExtensions.cs
new file mode 100644
index 00000000..e03d3ed5
--- /dev/null
+++ b/src/redmine-net-api/Http/Extensions/RedmineApiResponseExtensions.cs
@@ -0,0 +1,55 @@
+/*
+ Copyright 2011 - 2025 Adrian Popescu
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+using System.Collections.Generic;
+using System.Text;
+using Redmine.Net.Api.Common;
+using Redmine.Net.Api.Extensions;
+using Redmine.Net.Api.Http.Messages;
+using Redmine.Net.Api.Serialization;
+
+namespace Redmine.Net.Api.Http.Extensions;
+
+internal static class RedmineApiResponseExtensions
+{
+ internal static T DeserializeTo(this RedmineApiResponse responseMessage, IRedmineSerializer redmineSerializer) where T : new()
+ {
+ var responseAsString = GetResponseContentAsString(responseMessage);
+ return responseAsString.IsNullOrWhiteSpace() ? default : redmineSerializer.Deserialize(responseAsString);
+ }
+
+ internal static PagedResults DeserializeToPagedResults(this RedmineApiResponse responseMessage, IRedmineSerializer redmineSerializer) where T : class, new()
+ {
+ var responseAsString = GetResponseContentAsString(responseMessage);
+ return responseAsString.IsNullOrWhiteSpace() ? default : redmineSerializer.DeserializeToPagedResults(responseAsString);
+ }
+
+ internal static List DeserializeToList(this RedmineApiResponse responseMessage, IRedmineSerializer redmineSerializer) where T : class, new()
+ {
+ var responseAsString = GetResponseContentAsString(responseMessage);
+ return responseAsString.IsNullOrWhiteSpace() ? null : redmineSerializer.Deserialize>(responseAsString);
+ }
+
+ ///
+ /// Gets the response content as a UTF-8 encoded string.
+ ///
+ /// The API response message.
+ /// The content as a string, or null if the response or content is null.
+ private static string GetResponseContentAsString(RedmineApiResponse responseMessage)
+ {
+ return responseMessage?.Content == null ? null : Encoding.UTF8.GetString(responseMessage.Content);
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs b/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs
new file mode 100644
index 00000000..4e0aa252
--- /dev/null
+++ b/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using Redmine.Net.Api.Exceptions;
+using Redmine.Net.Api.Extensions;
+using Redmine.Net.Api.Http.Constants;
+using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Types;
+
+namespace Redmine.Net.Api.Http.Helpers;
+
+///
+/// Handles HTTP status codes and converts them to appropriate Redmine exceptions.
+///
+internal static class RedmineExceptionHelper
+{
+ ///
+ /// Maps an HTTP status code to an appropriate Redmine exception.
+ ///
+ /// The HTTP status code.
+ /// The response stream containing error details.
+ /// The inner exception, if any.
+ /// The serializer to use for deserializing error messages.
+ /// A specific Redmine exception based on the status code.
+ internal static void MapStatusCodeToException(int statusCode, Stream responseStream, Exception inner, IRedmineSerializer serializer)
+ {
+ switch (statusCode)
+ {
+ case HttpConstants.StatusCodes.NotFound:
+ throw new NotFoundException(HttpConstants.ErrorMessages.NotFound, inner);
+
+ case HttpConstants.StatusCodes.Unauthorized:
+ throw new UnauthorizedException(HttpConstants.ErrorMessages.Unauthorized, inner);
+
+ case HttpConstants.StatusCodes.Forbidden:
+ throw new ForbiddenException(HttpConstants.ErrorMessages.Forbidden, inner);
+
+ case HttpConstants.StatusCodes.Conflict:
+ throw new ConflictException(HttpConstants.ErrorMessages.Conflict, inner);
+
+ case HttpConstants.StatusCodes.UnprocessableEntity:
+ throw CreateUnprocessableEntityException(responseStream, inner, serializer);
+
+ case HttpConstants.StatusCodes.NotAcceptable:
+ throw new NotAcceptableException(HttpConstants.ErrorMessages.NotAcceptable, inner);
+
+ case HttpConstants.StatusCodes.InternalServerError:
+ throw new InternalServerErrorException(HttpConstants.ErrorMessages.InternalServerError, inner);
+
+ default:
+ throw new RedmineException($"HTTP {statusCode} – {(HttpStatusCode)statusCode}", inner);
+ }
+ }
+
+ ///
+ /// Creates an exception for a 422 Unprocessable Entity response.
+ ///
+ /// The response stream containing error details.
+ /// The inner exception, if any.
+ /// The serializer to use for deserializing error messages.
+ /// A RedmineException with details about the validation errors.
+ private static RedmineException CreateUnprocessableEntityException(Stream responseStream, Exception inner, IRedmineSerializer serializer)
+ {
+ var errors = GetRedmineErrors(responseStream, serializer);
+
+ if (errors is null)
+ {
+ return new RedmineException(HttpConstants.ErrorMessages.UnprocessableEntity, inner);
+ }
+
+ var sb = new StringBuilder();
+ foreach (var error in errors)
+ {
+ sb.Append(error.Info);
+ sb.Append(Environment.NewLine);
+ }
+
+ if (sb.Length > 0)
+ {
+ sb.Length -= 1;
+ }
+
+ return new RedmineException($"Unprocessable Content: {sb}", inner);
+ }
+
+ ///
+ /// Gets the Redmine errors from a response stream.
+ ///
+ /// The response stream containing error details.
+ /// The serializer to use for deserializing error messages.
+ /// A list of error objects or null if unable to parse errors.
+ private static List GetRedmineErrors(Stream responseStream, IRedmineSerializer serializer)
+ {
+ if (responseStream == null)
+ {
+ return null;
+ }
+
+ using (responseStream)
+ {
+ try
+ {
+ using var reader = new StreamReader(responseStream);
+ var content = reader.ReadToEnd();
+ return GetRedmineErrors(content, serializer);
+ }
+ catch(Exception ex)
+ {
+ throw new RedmineApiException(ex.Message, ex);
+ }
+ }
+ }
+
+ ///
+ /// Gets the Redmine errors from response content.
+ ///
+ /// The response content as a string.
+ /// The serializer to use for deserializing error messages.
+ /// A list of error objects or null if unable to parse errors.
+ private static List GetRedmineErrors(string content, IRedmineSerializer serializer)
+ {
+ if (content.IsNullOrWhiteSpace())
+ {
+ return null;
+ }
+
+ try
+ {
+ var paged = serializer.DeserializeToPagedResults(content);
+ return (List)paged.Items;
+ }
+ catch(Exception ex)
+ {
+ throw new RedmineException(ex.Message, ex);
+ }
+ }
+}
diff --git a/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs b/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs
new file mode 100644
index 00000000..c3fc7e10
--- /dev/null
+++ b/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs
@@ -0,0 +1,63 @@
+using System;
+using Redmine.Net.Api.Http.Constants;
+#if !NET20
+using System.Net.Http;
+#endif
+
+namespace Redmine.Net.Api.Http.Helpers;
+
+internal static class RedmineHttpMethodHelper
+{
+#if !NET20
+ private static readonly HttpMethod PatchMethod = new HttpMethod("PATCH");
+ private static readonly HttpMethod DownloadMethod = new HttpMethod("DOWNLOAD");
+
+ ///
+ /// Gets an HttpMethod instance for the specified HTTP verb.
+ ///
+ /// The HTTP verb (GET, POST, etc.).
+ /// An HttpMethod instance corresponding to the verb.
+ /// Thrown when the verb is not supported.
+ public static HttpMethod GetHttpMethod(string verb)
+ {
+ return verb switch
+ {
+ HttpConstants.HttpVerbs.GET => HttpMethod.Get,
+ HttpConstants.HttpVerbs.POST => HttpMethod.Post,
+ HttpConstants.HttpVerbs.PUT => HttpMethod.Put,
+ HttpConstants.HttpVerbs.PATCH => PatchMethod,
+ HttpConstants.HttpVerbs.DELETE => HttpMethod.Delete,
+ HttpConstants.HttpVerbs.DOWNLOAD => DownloadMethod,
+ _ => throw new ArgumentException($"Unsupported HTTP verb: {verb}")
+ };
+ }
+#endif
+ ///
+ /// Determines whether the specified HTTP method is a GET or DOWNLOAD method.
+ ///
+ /// The HTTP method to check.
+ /// True if the method is GET or DOWNLOAD; otherwise, false.
+ public static bool IsGetOrDownload(string method)
+ {
+ return method == HttpConstants.HttpVerbs.GET || method == HttpConstants.HttpVerbs.DOWNLOAD;
+ }
+
+ ///
+ /// Determines whether the HTTP status code represents a transient error.
+ ///
+ /// The HTTP response status code.
+ /// True if the status code represents a transient error; otherwise, false.
+ private static bool IsTransientError(int statusCode)
+ {
+ return statusCode switch
+ {
+ HttpConstants.StatusCodes.BadGateway => true,
+ HttpConstants.StatusCodes.GatewayTimeout => true,
+ HttpConstants.StatusCodes.ServiceUnavailable => true,
+ HttpConstants.StatusCodes.RequestTimeout => true,
+ HttpConstants.StatusCodes.TooManyRequests => true,
+ _ => false
+ };
+ }
+
+}
diff --git a/src/redmine-net-api/Http/IRedmineApiClient.cs b/src/redmine-net-api/Http/IRedmineApiClient.cs
new file mode 100644
index 00000000..203d9599
--- /dev/null
+++ b/src/redmine-net-api/Http/IRedmineApiClient.cs
@@ -0,0 +1,75 @@
+/*
+ Copyright 2011 - 2025 Adrian Popescu
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+using System;
+using Redmine.Net.Api.Http.Messages;
+#if !NET20
+using System.Threading;
+using System.Threading.Tasks;
+#endif
+using Redmine.Net.Api.Net;
+using Redmine.Net.Api.Net.Internal;
+
+namespace Redmine.Net.Api.Http;
+///
+///
+///
+internal interface IRedmineApiClient : ISyncRedmineApiClient
+#if !NET20
+ , IAsyncRedmineApiClient
+#endif
+{
+}
+
+internal interface ISyncRedmineApiClient
+{
+ RedmineApiResponse Get(string address, RequestOptions requestOptions = null);
+
+ RedmineApiResponse GetPaged(string address, RequestOptions requestOptions = null);
+
+ RedmineApiResponse Create(string address, string payload, RequestOptions requestOptions = null);
+
+ RedmineApiResponse Update(string address, string payload, RequestOptions requestOptions = null);
+
+ RedmineApiResponse Patch(string address, string payload, RequestOptions requestOptions = null);
+
+ RedmineApiResponse Delete(string address, RequestOptions requestOptions = null);
+
+ RedmineApiResponse Upload(string address, byte[] data, RequestOptions requestOptions = null);
+
+ RedmineApiResponse Download(string address, RequestOptions requestOptions = null, IProgress progress = null);
+}
+
+#if !NET20
+internal interface IAsyncRedmineApiClient
+{
+ Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
+
+ Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
+
+ Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
+
+ Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
+
+ Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
+
+ Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
+
+ Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
+
+ Task DownloadAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default);
+}
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/IRedmineApiClientOptions.cs b/src/redmine-net-api/Http/IRedmineApiClientOptions.cs
new file mode 100644
index 00000000..44089261
--- /dev/null
+++ b/src/redmine-net-api/Http/IRedmineApiClientOptions.cs
@@ -0,0 +1,148 @@
+/*
+ Copyright 2011 - 2025 Adrian Popescu
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Cache;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Redmine.Net.Api.Http
+{
+ ///
+ ///
+ ///
+ public interface IRedmineApiClientOptions
+ {
+ ///
+ ///
+ ///
+ bool? AutoRedirect { get; set; }
+
+ ///
+ ///
+ ///
+ CookieContainer CookieContainer { get; set; }
+
+ ///
+ ///
+ ///
+ DecompressionMethods? DecompressionFormat { get; set; }
+
+ ///
+ ///
+ ///
+ ICredentials Credentials { get; set; }
+
+ ///
+ ///
+ ///
+ Dictionary DefaultHeaders { get; set; }
+
+ ///
+ ///
+ ///
+ IWebProxy Proxy { get; set; }
+
+ ///
+ ///
+ ///
+ int? MaxAutomaticRedirections { get; set; }
+
+#if NET471_OR_GREATER || NET
+ ///
+ ///
+ ///
+ int? MaxConnectionsPerServer { get; set; }
+
+ ///
+ ///
+ ///
+ int? MaxResponseHeadersLength { get; set; }
+#endif
+ ///
+ ///
+ ///
+ bool? PreAuthenticate { get; set; }
+
+ ///
+ ///
+ ///
+ RequestCachePolicy RequestCachePolicy { get; set; }
+
+ ///
+ ///
+ ///
+ string Scheme { get; set; }
+
+ ///
+ ///
+ ///
+ TimeSpan? Timeout { get; set; }
+
+ ///
+ ///
+ ///
+ string UserAgent { get; set; }
+
+ ///
+ ///
+ ///
+ bool? UseCookies { get; set; }
+
+#if NETFRAMEWORK
+
+ ///
+ ///
+ ///
+ bool CheckCertificateRevocationList { get; set; }
+
+ ///
+ ///
+ ///
+ long? MaxRequestContentBufferSize { get; set; }
+
+ ///
+ ///
+ ///
+ long? MaxResponseContentBufferSize { get; set; }
+
+ ///
+ ///
+ ///
+ bool? UseDefaultCredentials { get; set; }
+#endif
+ ///
+ ///
+ ///
+ bool? UseProxy { get; set; }
+
+ ///
+ ///
+ ///
+ /// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported.
+ Version ProtocolVersion { get; set; }
+
+
+#if NET40_OR_GREATER || NET
+ ///
+ ///
+ ///
+ public X509CertificateCollection ClientCertificates { get; set; }
+#endif
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/ApiRequestMessage.cs b/src/redmine-net-api/Http/Messages/RedmineApiRequest.cs
similarity index 74%
rename from src/redmine-net-api/Net/ApiRequestMessage.cs
rename to src/redmine-net-api/Http/Messages/RedmineApiRequest.cs
index b0b7a2fb..ad42592d 100644
--- a/src/redmine-net-api/Net/ApiRequestMessage.cs
+++ b/src/redmine-net-api/Http/Messages/RedmineApiRequest.cs
@@ -15,13 +15,15 @@ limitations under the License.
*/
using System.Collections.Specialized;
+using Redmine.Net.Api.Http.Clients.WebClient;
+using Redmine.Net.Api.Http.Constants;
-namespace Redmine.Net.Api.Net;
+namespace Redmine.Net.Api.Http.Messages;
-internal sealed class ApiRequestMessage
+internal sealed class RedmineApiRequest
{
- public ApiRequestMessageContent Content { get; set; }
- public string Method { get; set; } = HttpVerbs.GET;
+ public RedmineApiRequestContent Content { get; set; }
+ public string Method { get; set; } = HttpConstants.HttpVerbs.GET;
public string RequestUri { get; set; }
public NameValueCollection QueryString { get; set; }
public string ImpersonateUser { get; set; }
diff --git a/src/redmine-net-api/Net/ApiResponseMessage.cs b/src/redmine-net-api/Http/Messages/RedmineApiResponse.cs
similarity index 82%
rename from src/redmine-net-api/Net/ApiResponseMessage.cs
rename to src/redmine-net-api/Http/Messages/RedmineApiResponse.cs
index 971aaabb..71fb1948 100644
--- a/src/redmine-net-api/Net/ApiResponseMessage.cs
+++ b/src/redmine-net-api/Http/Messages/RedmineApiResponse.cs
@@ -15,11 +15,15 @@ limitations under the License.
*/
using System.Collections.Specialized;
+using System.Net;
-namespace Redmine.Net.Api.Net;
+namespace Redmine.Net.Api.Http.Messages;
-internal sealed class ApiResponseMessage
+internal sealed class RedmineApiResponse
{
public NameValueCollection Headers { get; init; }
public byte[] Content { get; init; }
+
+ public HttpStatusCode StatusCode { get; init; }
+
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/RedirectType.cs b/src/redmine-net-api/Http/RedirectType.cs
similarity index 93%
rename from src/redmine-net-api/Net/RedirectType.cs
rename to src/redmine-net-api/Http/RedirectType.cs
index ae9aedb5..5bb7acf7 100644
--- a/src/redmine-net-api/Net/RedirectType.cs
+++ b/src/redmine-net-api/Http/RedirectType.cs
@@ -14,12 +14,12 @@ You may obtain a copy of the License at
limitations under the License.
*/
-namespace Redmine.Net.Api
+namespace Redmine.Net.Api.Http
{
///
///
///
- public enum RedirectType
+ internal enum RedirectType
{
///
///
diff --git a/src/redmine-net-api/Http/RedmineApiClient.Async.cs b/src/redmine-net-api/Http/RedmineApiClient.Async.cs
new file mode 100644
index 00000000..e30fb302
--- /dev/null
+++ b/src/redmine-net-api/Http/RedmineApiClient.Async.cs
@@ -0,0 +1,77 @@
+#if !NET20
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Redmine.Net.Api.Http.Constants;
+using Redmine.Net.Api.Http.Messages;
+using Redmine.Net.Api.Net;
+using Redmine.Net.Api.Net.Internal;
+
+namespace Redmine.Net.Api.Http;
+
+internal abstract partial class RedmineApiClient
+{
+ public async Task GetAsync(string address, RequestOptions requestOptions = null,
+ CancellationToken cancellationToken = default)
+ {
+ return await HandleRequestAsync(address, HttpConstants.HttpVerbs.GET, requestOptions, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ public async Task GetPagedAsync(string address, RequestOptions requestOptions = null,
+ CancellationToken cancellationToken = default)
+ {
+ return await GetAsync(address, requestOptions, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task CreateAsync(string address, string payload,
+ RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
+ {
+ return await HandleRequestAsync(address, HttpConstants.HttpVerbs.POST, requestOptions, CreateContentFromPayload(payload),
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task UpdateAsync(string address, string payload,
+ RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
+ {
+ return await HandleRequestAsync(address, HttpConstants.HttpVerbs.PUT, requestOptions, CreateContentFromPayload(payload),
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task UploadFileAsync(string address, byte[] data,
+ RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
+ {
+ return await HandleRequestAsync(address, HttpConstants.HttpVerbs.POST, requestOptions, CreateContentFromBytes(data),
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task PatchAsync(string address, string payload,
+ RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
+ {
+ return await HandleRequestAsync(address, HttpConstants.HttpVerbs.PATCH, requestOptions, CreateContentFromPayload(payload),
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task DeleteAsync(string address, RequestOptions requestOptions = null,
+ CancellationToken cancellationToken = default)
+ {
+ return await HandleRequestAsync(address, HttpConstants.HttpVerbs.DELETE, requestOptions, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ public async Task DownloadAsync(string address, RequestOptions requestOptions = null,
+ IProgress progress = null, CancellationToken cancellationToken = default)
+ {
+ return await HandleRequestAsync(address, HttpConstants.HttpVerbs.DOWNLOAD, requestOptions, progress: progress,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ protected abstract Task HandleRequestAsync(
+ string address,
+ string verb,
+ RequestOptions requestOptions = null,
+ object content = null,
+ IProgress progress = null,
+ CancellationToken cancellationToken = default);
+}
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Http/RedmineApiClient.cs b/src/redmine-net-api/Http/RedmineApiClient.cs
new file mode 100644
index 00000000..c105e59e
--- /dev/null
+++ b/src/redmine-net-api/Http/RedmineApiClient.cs
@@ -0,0 +1,113 @@
+using System;
+using Redmine.Net.Api.Authentication;
+using Redmine.Net.Api.Http.Constants;
+using Redmine.Net.Api.Http.Messages;
+using Redmine.Net.Api.Net;
+using Redmine.Net.Api.Net.Internal;
+using Redmine.Net.Api.Options;
+using Redmine.Net.Api.Serialization;
+
+namespace Redmine.Net.Api.Http;
+
+internal abstract partial class RedmineApiClient : IRedmineApiClient
+{
+ protected readonly IRedmineAuthentication Credentials;
+ protected readonly IRedmineSerializer Serializer;
+ protected readonly RedmineManagerOptions Options;
+
+ protected RedmineApiClient(RedmineManagerOptions redmineManagerOptions)
+ {
+ Credentials = redmineManagerOptions.Authentication;
+ Serializer = redmineManagerOptions.Serializer;
+ Options = redmineManagerOptions;
+ }
+
+ public RedmineApiResponse Get(string address, RequestOptions requestOptions = null)
+ {
+ return HandleRequest(address, HttpConstants.HttpVerbs.GET, requestOptions);
+ }
+
+ public RedmineApiResponse GetPaged(string address, RequestOptions requestOptions = null)
+ {
+ return Get(address, requestOptions);
+ }
+
+ public RedmineApiResponse Create(string address, string payload, RequestOptions requestOptions = null)
+ {
+ return HandleRequest(address, HttpConstants.HttpVerbs.POST, requestOptions, CreateContentFromPayload(payload));
+ }
+
+ public RedmineApiResponse Update(string address, string payload, RequestOptions requestOptions = null)
+ {
+ return HandleRequest(address, HttpConstants.HttpVerbs.PUT, requestOptions, CreateContentFromPayload(payload));
+ }
+
+ public RedmineApiResponse Patch(string address, string payload, RequestOptions requestOptions = null)
+ {
+ return HandleRequest(address, HttpConstants.HttpVerbs.PATCH, requestOptions, CreateContentFromPayload(payload));
+ }
+
+ public RedmineApiResponse Delete(string address, RequestOptions requestOptions = null)
+ {
+ return HandleRequest(address, HttpConstants.HttpVerbs.DELETE, requestOptions);
+ }
+
+ public RedmineApiResponse Download(string address, RequestOptions requestOptions = null,
+ IProgress progress = null)
+ {
+ return HandleRequest(address, HttpConstants.HttpVerbs.DOWNLOAD, requestOptions, progress: progress);
+ }
+
+ public RedmineApiResponse Upload(string address, byte[] data, RequestOptions requestOptions = null)
+ {
+ return HandleRequest(address, HttpConstants.HttpVerbs.POST, requestOptions, CreateContentFromBytes(data));
+ }
+
+ protected abstract RedmineApiResponse HandleRequest(
+ string address,
+ string verb,
+ RequestOptions requestOptions = null,
+ object content = null,
+ IProgress progress = null);
+
+ protected abstract object CreateContentFromPayload(string payload);
+
+ protected abstract object CreateContentFromBytes(byte[] data);
+
+ protected static bool IsGetOrDownload(string method)
+ {
+ return method is HttpConstants.HttpVerbs.GET or HttpConstants.HttpVerbs.DOWNLOAD;
+ }
+
+ protected static void ReportProgress(IProgressprogress, long total, long bytesRead)
+ {
+ if (progress == null || total <= 0)
+ {
+ return;
+ }
+ var percent = (int)(bytesRead * 100L / total);
+ progress.Report(percent);
+ }
+
+ // protected void LogRequest(string verb, string address, RequestOptions requestOptions)
+ // {
+ // if (_options.LoggingOptions?.IncludeHttpDetails == true)
+ // {
+ // _options.Logger.Debug($"Request HTTP {verb} {address}");
+ //
+ // if (requestOptions?.QueryString != null)
+ // {
+ // _options.Logger.Debug($"Query parameters: {requestOptions.QueryString.ToQueryString()}");
+ // }
+ // }
+ // }
+ //
+ // protected void LogResponse(HttpStatusCode statusCode)
+ // {
+ // if (_options.LoggingOptions?.IncludeHttpDetails == true)
+ // {
+ // _options.Logger.Debug($"Response status: {statusCode}");
+ // }
+ // }
+
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs b/src/redmine-net-api/Http/RedmineApiClientOptions.cs
similarity index 56%
rename from src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs
rename to src/redmine-net-api/Http/RedmineApiClientOptions.cs
index 714df02d..1cf7f6cc 100644
--- a/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs
+++ b/src/redmine-net-api/Http/RedmineApiClientOptions.cs
@@ -1,31 +1,19 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Cache;
+#if NET || NET471_OR_GREATER
+using System.Net.Http;
+#endif
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
-namespace Redmine.Net.Api.Net.WebClient;
+namespace Redmine.Net.Api.Http;
+
///
///
///
-public sealed class RedmineWebClientOptions: IRedmineWebClientOptions
+public abstract class RedmineApiClientOptions : IRedmineApiClientOptions
{
///
///
@@ -40,7 +28,12 @@ public sealed class RedmineWebClientOptions: IRedmineWebClientOptions
///
///
///
- public DecompressionMethods? DecompressionFormat { get; set; }
+ public DecompressionMethods? DecompressionFormat { get; set; } =
+#if NET
+ DecompressionMethods.All;
+#else
+ DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.None;
+#endif
///
///
@@ -57,20 +50,12 @@ public sealed class RedmineWebClientOptions: IRedmineWebClientOptions
///
public IWebProxy Proxy { get; set; }
- ///
- ///
- ///
- public bool? KeepAlive { get; set; }
-
///
///
///
public int? MaxAutomaticRedirections { get; set; }
- ///
- ///
- ///
- public long? MaxRequestContentBufferSize { get; set; }
+
///
///
@@ -102,36 +87,38 @@ public sealed class RedmineWebClientOptions: IRedmineWebClientOptions
///
public string Scheme { get; set; } = "https";
+
///
///
///
- public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; }
+ public TimeSpan? Timeout { get; set; } = TimeSpan.FromSeconds(30);
///
///
///
- public TimeSpan? Timeout { get; set; }
+ public string UserAgent { get; set; } = "RedmineDotNetAPIClient";
///
///
///
- public bool? UnsafeAuthenticatedConnectionSharing { get; set; }
+ public bool? UseCookies { get; set; }
- ///
+#if NETFRAMEWORK
+ ///
///
///
- public string UserAgent { get; set; } = "RedmineDotNetAPIClient";
+ public bool CheckCertificateRevocationList { get; set; }
- ///
+ ///
///
///
- public bool? UseCookies { get; set; }
+ public long? MaxRequestContentBufferSize { get; set; }
///
///
///
public bool? UseDefaultCredentials { get; set; }
-
+#endif
///
///
///
@@ -143,53 +130,15 @@ public sealed class RedmineWebClientOptions: IRedmineWebClientOptions
/// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported.
public Version ProtocolVersion { get; set; }
+
- #if NET40_OR_GREATER || NETCOREAPP
- ///
- ///
- ///
- public X509CertificateCollection ClientCertificates { get; set; }
- #endif
-
- ///
- ///
- ///
- public bool CheckCertificateRevocationList { get; set; }
-
- ///
- ///
- ///
- public int? DefaultConnectionLimit { get; set; }
-
- ///
- ///
- ///
- public int? DnsRefreshTimeout { get; set; }
-
- ///
- ///
- ///
- public bool? EnableDnsRoundRobin { get; set; }
-
- ///
- ///
- ///
- public int? MaxServicePoints { get; set; }
-
- ///
- ///
- ///
- public int? MaxServicePointIdleTime { get; set; }
- #if(NET46_OR_GREATER || NETCOREAPP)
+#if NET40_OR_GREATER || NETCOREAPP
///
///
///
- public bool? ReusePort { get; set; }
- #endif
+ public X509CertificateCollection ClientCertificates { get; set; }
+#endif
- ///
- ///
- ///
- public SecurityProtocolType? SecurityProtocolType { get; set; }
+
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/RequestOptions.cs b/src/redmine-net-api/Http/RequestOptions.cs
similarity index 64%
rename from src/redmine-net-api/Net/RequestOptions.cs
rename to src/redmine-net-api/Http/RequestOptions.cs
index 1c31f7a6..1e06ae53 100644
--- a/src/redmine-net-api/Net/RequestOptions.cs
+++ b/src/redmine-net-api/Http/RequestOptions.cs
@@ -14,9 +14,11 @@ You may obtain a copy of the License at
limitations under the License.
*/
+using System.Collections.Generic;
using System.Collections.Specialized;
+using Redmine.Net.Api.Extensions;
-namespace Redmine.Net.Api.Net;
+namespace Redmine.Net.Api.Http;
///
///
@@ -43,6 +45,11 @@ public sealed class RequestOptions
///
///
public string UserAgent { get; set; }
+
+ ///
+ ///
+ ///
+ public Dictionary Headers { get; set; }
///
///
@@ -56,7 +63,31 @@ public RequestOptions Clone()
ImpersonateUser = ImpersonateUser,
ContentType = ContentType,
Accept = Accept,
- UserAgent = UserAgent
+ UserAgent = UserAgent,
+ Headers = Headers != null ? new Dictionary(Headers) : null,
+ };
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static RequestOptions Include(string include)
+ {
+ if (include.IsNullOrWhiteSpace())
+ {
+ return null;
+ }
+
+ var requestOptions = new RequestOptions
+ {
+ QueryString = new NameValueCollection
+ {
+ {RedmineKeys.INCLUDE, include}
+ }
};
+
+ return requestOptions;
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/IRedmineManagerAsync.cs b/src/redmine-net-api/IRedmineManager.Async.cs
similarity index 97%
rename from src/redmine-net-api/IRedmineManagerAsync.cs
rename to src/redmine-net-api/IRedmineManager.Async.cs
index 4d2343d6..0cb733b5 100644
--- a/src/redmine-net-api/IRedmineManagerAsync.cs
+++ b/src/redmine-net-api/IRedmineManager.Async.cs
@@ -15,9 +15,12 @@ limitations under the License.
*/
#if !(NET20)
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using Redmine.Net.Api.Common;
+using Redmine.Net.Api.Http;
using Redmine.Net.Api.Net;
using Redmine.Net.Api.Serialization;
using Redmine.Net.Api.Types;
@@ -147,9 +150,10 @@ Task DeleteAsync(string id, RequestOptions requestOptions = null, Cancellatio
///
/// The address.
///
+ ///
///
///
- Task DownloadFileAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
+ Task DownloadFileAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default);
}
}
#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/IRedmineManager.Obsolete.cs b/src/redmine-net-api/IRedmineManager.Obsolete.cs
deleted file mode 100644
index 028984a3..00000000
--- a/src/redmine-net-api/IRedmineManager.Obsolete.cs
+++ /dev/null
@@ -1,306 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-using System.Net;
-using System.Net.Security;
-using System.Security.Cryptography.X509Certificates;
-using Redmine.Net.Api.Serialization;
-
-namespace Redmine.Net.Api.Types
-{
- ///
- ///
- ///
- public partial interface IRedmineManager
- {
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- string Host { get; }
-
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- string ApiKey { get; }
-
- ///
- /// Maximum page-size when retrieving complete object lists
- ///
- /// By default, only 25 results can be retrieved per request. Maximum is 100. To change the maximum value set
- /// in your Settings -> General, "Objects per page options".By adding (for instance) 9999 there would make you
- /// able to get that many results per request.
- ///
- ///
- ///
- /// The size of the page.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- int PageSize { get; set; }
-
- ///
- /// As of Redmine 2.2.0 you can impersonate user setting user login (e.g. jsmith). This only works when using the API
- /// with an administrator account, this header will be ignored when using the API with a regular user account.
- ///
- ///
- /// The impersonate user.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- string ImpersonateUser { get; set; }
-
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- MimeFormat MimeFormat { get; }
-
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- IWebProxy Proxy { get; }
-
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- SecurityProtocolType SecurityProtocolType { get; }
-
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'GetCurrentUser' extension instead")]
- User GetCurrentUser(NameValueCollection parameters = null);
-
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'AddUserToGroup' extension instead")]
- void AddUserToGroup(int groupId, int userId);
-
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'RemoveUserFromGroup' extension instead")]
- void RemoveUserFromGroup(int groupId, int userId);
-
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'AddWatcherToIssue' extension instead")]
- void AddWatcherToIssue(int issueId, int userId);
-
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'RemoveWatcherFromIssue' extension instead")]
- void RemoveWatcherFromIssue(int issueId, int userId);
-
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'CreateWikiPage' extension instead")]
- WikiPage CreateWikiPage(string projectId, string pageName, WikiPage wikiPage);
-
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'UpdateWikiPage' extension instead")]
- void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage);
-
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'GetWikiPage' extension instead")]
- WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0);
-
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'GetAllWikiPages' extension instead")]
- List GetAllWikiPages(string projectId);
-
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'DeleteWikiPage' extension instead")]
- void DeleteWikiPage(string projectId, string pageName);
-
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'UpdateAttachment' extension instead")]
- void UpdateAttachment(int issueId, Attachment attachment);
-
- ///
- ///
- ///
- /// query strings. enable to specify multiple values separated by a space " ".
- /// number of results in response.
- /// skip this number of results in response
- /// Optional filters.
- ///
- /// Returns the search results by the specified condition parameters.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Search' extension instead")]
- PagedResults Search(string q, int limit , int offset = 0, SearchFilterBuilder searchFilter = null);
-
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'GetPaginated' method instead")]
- PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new();
-
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Count' method instead")]
- int Count(params string[] include) where T : class, new();
-
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Get' method instead")]
- T GetObject(string id, NameValueCollection parameters) where T : class, new();
-
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Get' method instead")]
- List GetObjects(int limit, int offset, params string[] include) where T : class, new();
-
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Get' method instead")]
- List GetObjects(params string[] include) where T : class, new();
-
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Get' method instead")]
- List GetObjects(NameValueCollection parameters) where T : class, new();
-
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Create' method instead")]
- T CreateObject(T entity) where T : class, new();
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Create' method instead")]
- T CreateObject(T entity, string ownerId) where T : class, new();
-
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Update' method instead")]
- void UpdateObject(string id, T entity, string projectId = null) where T : class, new();
-
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Delete' method instead")]
- void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new();
-
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false);
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors);
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs
index 5e4f5437..e7f487dc 100644
--- a/src/redmine-net-api/IRedmineManager.cs
+++ b/src/redmine-net-api/IRedmineManager.cs
@@ -14,12 +14,14 @@ You may obtain a copy of the License at
limitations under the License.
*/
+using System;
using System.Collections.Generic;
+using Redmine.Net.Api.Common;
using Redmine.Net.Api.Exceptions;
-using Redmine.Net.Api.Net;
-using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Http;
+using Redmine.Net.Api.Types;
-namespace Redmine.Net.Api.Types;
+namespace Redmine.Net.Api;
///
///
@@ -96,21 +98,22 @@ void Delete(string id, RequestOptions requestOptions = null)
///
/// Support for adding attachments through the REST API is added in Redmine 1.4.0.
- /// Upload a file to server.
+ /// Upload a file to the server.
///
/// The content of the file that will be uploaded on server.
///
///
- /// Returns the token for uploaded file.
+ /// Returns the token for the uploaded file.
///
///
Upload UploadFile(byte[] data, string fileName = null);
-
+
///
/// Downloads a file from the specified address.
///
/// The address.
+ ///
/// The content of the downloaded file as a byte array.
///
- byte[] DownloadFile(string address);
+ byte[] DownloadFile(string address, IProgress progress = null);
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs b/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs
index 86f45107..e7daa84e 100644
--- a/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs
+++ b/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs
@@ -2,6 +2,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
+#nullable enable
+
namespace Redmine.Net.Api.Internals;
internal static class ArgumentNullThrowHelper
diff --git a/src/redmine-net-api/Internals/HostHelper.cs b/src/redmine-net-api/Internals/HostHelper.cs
new file mode 100644
index 00000000..e5a0fe0e
--- /dev/null
+++ b/src/redmine-net-api/Internals/HostHelper.cs
@@ -0,0 +1,172 @@
+using System;
+using Redmine.Net.Api.Exceptions;
+using Redmine.Net.Api.Extensions;
+
+namespace Redmine.Net.Api.Internals;
+
+internal static class HostHelper
+{
+ private static readonly char[] DotCharArray = ['.'];
+
+ internal static void EnsureDomainNameIsValid(string domainName)
+ {
+ if (domainName.IsNullOrWhiteSpace())
+ {
+ throw new RedmineException("Domain name cannot be null or empty.");
+ }
+
+ if (domainName.Length > 255)
+ {
+ throw new RedmineException("Domain name cannot be longer than 255 characters.");
+ }
+
+ var labels = domainName.Split(DotCharArray);
+ if (labels.Length == 1)
+ {
+ throw new RedmineException("Domain name is not valid.");
+ }
+
+ foreach (var label in labels)
+ {
+ if (label.IsNullOrWhiteSpace() || label.Length > 63)
+ {
+ throw new RedmineException("Domain name must be between 1 and 63 characters.");
+ }
+
+ if (!char.IsLetterOrDigit(label[0]) || !char.IsLetterOrDigit(label[label.Length - 1]))
+ {
+ throw new RedmineException("Domain name label starts or ends with a hyphen or invalid character.");
+ }
+
+ for (var index = 0; index < label.Length; index++)
+ {
+ var ch = label[index];
+
+ if (!char.IsLetterOrDigit(ch) && ch != '-')
+ {
+ throw new RedmineException("Domain name contains an invalid character.");
+ }
+
+ if (ch == '-' && index + 1 < label.Length && label[index + 1] == '-')
+ {
+ throw new RedmineException("Domain name contains consecutive hyphens.");
+ }
+ }
+ }
+ }
+
+ internal static Uri CreateRedmineUri(string host, string scheme = null)
+ {
+ if (host.IsNullOrWhiteSpace())
+ {
+ throw new RedmineException("The host is null or empty.");
+ }
+
+ if (!Uri.TryCreate(host, UriKind.Absolute, out var uri))
+ {
+ host = host.TrimEnd('/', '\\');
+ EnsureDomainNameIsValid(host);
+
+ if (!host.StartsWith(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
+ !host.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
+ {
+ host = $"{scheme ?? Uri.UriSchemeHttps}://{host}";
+
+ if (!Uri.TryCreate(host, UriKind.Absolute, out uri))
+ {
+ throw new RedmineException("The host is not valid.");
+ }
+ }
+ }
+
+ if (!uri.IsWellFormedOriginalString())
+ {
+ throw new RedmineException("The host is not well-formed.");
+ }
+
+ scheme ??= Uri.UriSchemeHttps;
+ var hasScheme = false;
+ if (!uri.Scheme.IsNullOrWhiteSpace())
+ {
+ if (uri.Host.IsNullOrWhiteSpace() && uri.IsAbsoluteUri && !uri.IsFile)
+ {
+ if (uri.Scheme.Equals("localhost", StringComparison.OrdinalIgnoreCase))
+ {
+ int port = 0;
+ var portAsString = uri.AbsolutePath.RemoveTrailingSlash();
+ if (!portAsString.IsNullOrWhiteSpace())
+ {
+ int.TryParse(portAsString, out port);
+ }
+
+ var ub = new UriBuilder(scheme, "localhost", port);
+ return ub.Uri;
+ }
+ }
+ else
+ {
+ if (!IsSchemaHttpOrHttps(uri.Scheme))
+ {
+ throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported.");
+ }
+
+ hasScheme = true;
+ }
+ }
+ else
+ {
+ if (!IsSchemaHttpOrHttps(scheme))
+ {
+ throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported.");
+ }
+ }
+
+ var uriBuilder = new UriBuilder();
+
+ if (uri.HostNameType == UriHostNameType.IPv6)
+ {
+ uriBuilder.Scheme = (hasScheme ? uri.Scheme : scheme ?? Uri.UriSchemeHttps);
+ uriBuilder.Host = uri.Host;
+ }
+ else
+ {
+ if (uri.Authority.IsNullOrWhiteSpace())
+ {
+ if (uri.Port == -1)
+ {
+ if (int.TryParse(uri.LocalPath, out var port))
+ {
+ uriBuilder.Port = port;
+ }
+ }
+
+ uriBuilder.Scheme = scheme ?? Uri.UriSchemeHttps;
+ uriBuilder.Host = uri.Scheme;
+ }
+ else
+ {
+ uriBuilder.Scheme = uri.Scheme;
+ uriBuilder.Port = int.TryParse(uri.LocalPath, out var port) ? port : uri.Port;
+ uriBuilder.Host = uri.Host;
+ if (!uri.LocalPath.IsNullOrWhiteSpace() && !uri.LocalPath.Contains("."))
+ {
+ uriBuilder.Path = uri.LocalPath;
+ }
+ }
+ }
+
+ try
+ {
+ return uriBuilder.Uri;
+ }
+ catch (Exception ex)
+ {
+ throw new RedmineException($"Failed to create Redmine URI: {ex.Message}", ex);
+ }
+ }
+
+ private static bool IsSchemaHttpOrHttps(string scheme)
+ {
+ return scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeHttps;
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/ColorConsoleLogger.cs b/src/redmine-net-api/Logging/ColorConsoleLogger.cs
deleted file mode 100644
index e7959dc9..00000000
--- a/src/redmine-net-api/Logging/ColorConsoleLogger.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- Copyright 2011 - 2019 Adrian Popescu.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-
-namespace Redmine.Net.Api.Logging
-{
- ///
- ///
- ///
- ///
- public sealed class ColorConsoleLogger : ILogger
- {
- private static readonly object locker = new object();
- private readonly ConsoleColor? defaultConsoleColor = null;
-
- ///
- ///
- ///
- public void Log(LogEntry entry)
- {
- lock (locker)
- {
- var colors = GetLogLevelConsoleColors(entry.Severity);
- switch (entry.Severity)
- {
- case LoggingEventType.Debug:
- Console.WriteLine(entry.Message, colors.Background, colors.Foreground);
- break;
- case LoggingEventType.Information:
- Console.WriteLine(entry.Message, colors.Background, colors.Foreground);
- break;
- case LoggingEventType.Warning:
- Console.WriteLine(entry.Message, colors.Background, colors.Foreground);
- break;
- case LoggingEventType.Error:
- Console.WriteLine(entry.Message, colors.Background, colors.Foreground);
- break;
- case LoggingEventType.Fatal:
- Console.WriteLine(entry.Message, colors.Background, colors.Foreground);
- break;
- }
- }
- }
-
- ///
- /// Gets the log level console colors.
- ///
- /// The log level.
- ///
- private ConsoleColors GetLogLevelConsoleColors(LoggingEventType logLevel)
- {
- // do not change user's background color except for Critical
- switch (logLevel)
- {
- case LoggingEventType.Fatal:
- return new ConsoleColors(ConsoleColor.White, ConsoleColor.Red);
- case LoggingEventType.Error:
- return new ConsoleColors(ConsoleColor.Red, defaultConsoleColor);
- case LoggingEventType.Warning:
- return new ConsoleColors(ConsoleColor.DarkYellow, defaultConsoleColor);
- case LoggingEventType.Information:
- return new ConsoleColors(ConsoleColor.DarkGreen, defaultConsoleColor);
- default:
- return new ConsoleColors(ConsoleColor.Gray, defaultConsoleColor);
- }
- }
-
- ///
- ///
- ///
- private struct ConsoleColors
- {
- public ConsoleColors(ConsoleColor? foreground, ConsoleColor? background): this()
- {
- Foreground = foreground;
- Background = background;
- }
-
- ///
- /// Gets or sets the foreground.
- ///
- ///
- /// The foreground.
- ///
- public ConsoleColor? Foreground { get; private set; }
-
- ///
- /// Gets or sets the background.
- ///
- ///
- /// The background.
- ///
- public ConsoleColor? Background { get; private set; }
- }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/ConsoleLogger.cs b/src/redmine-net-api/Logging/ConsoleLogger.cs
deleted file mode 100644
index 015b465a..00000000
--- a/src/redmine-net-api/Logging/ConsoleLogger.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- Copyright 2011 - 2019 Adrian Popescu.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-
-namespace Redmine.Net.Api.Logging
-{
- ///
- ///
- ///
- ///
- public sealed class ConsoleLogger : ILogger
- {
- private static readonly object locker = new object();
- ///
- ///
- ///
- public void Log(LogEntry entry)
- {
- lock (locker)
- {
- switch (entry.Severity)
- {
- case LoggingEventType.Debug:
- Console.WriteLine(entry.Message);
- break;
- case LoggingEventType.Information:
- Console.WriteLine(entry.Message);
- break;
- case LoggingEventType.Warning:
- Console.WriteLine(entry.Message);
- break;
- case LoggingEventType.Error:
- Console.WriteLine(entry.Message);
- break;
- case LoggingEventType.Fatal:
- Console.WriteLine(entry.Message);
- break;
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/ILogger.cs b/src/redmine-net-api/Logging/ILogger.cs
deleted file mode 100755
index 5d483046..00000000
--- a/src/redmine-net-api/Logging/ILogger.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- Copyright 2011 - 2019 Adrian Popescu.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-namespace Redmine.Net.Api.Logging
-{
- ///
- ///
- ///
- public interface ILogger
- {
- ///
- /// Logs the specified entry.
- ///
- /// The entry.
- void Log(LogEntry entry);
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/IRedmineLogger.cs b/src/redmine-net-api/Logging/IRedmineLogger.cs
new file mode 100644
index 00000000..47b4c980
--- /dev/null
+++ b/src/redmine-net-api/Logging/IRedmineLogger.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+
+namespace Redmine.Net.Api.Logging;
+
+///
+/// Provides abstraction for logging operations
+///
+public interface IRedmineLogger
+{
+ ///
+ /// Checks if the specified log level is enabled
+ ///
+ bool IsEnabled(LogLevel level);
+
+ ///
+ /// Logs a message with the specified level
+ ///
+ void Log(LogLevel level, string message, Exception exception = null);
+
+ ///
+ /// Creates a scoped logger with additional context
+ ///
+ IRedmineLogger CreateScope(string scopeName, IDictionary scopeProperties = null);
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/LogEntry.cs b/src/redmine-net-api/Logging/LogEntry.cs
deleted file mode 100644
index c91218cc..00000000
--- a/src/redmine-net-api/Logging/LogEntry.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- Copyright 2011 - 2019 Adrian Popescu.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-
-namespace Redmine.Net.Api.Logging
-{
- ///
- ///
- ///
- public sealed class LogEntry
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The severity.
- /// The message.
- /// The exception.
- public LogEntry(LoggingEventType severity, string message, Exception exception = null)
- {
- Severity = severity;
- Message = message;
- Exception = exception;
- }
-
- ///
- /// Gets the severity.
- ///
- ///
- /// The severity.
- ///
- public LoggingEventType Severity { get; private set; }
- ///
- /// Gets the message.
- ///
- ///
- /// The message.
- ///
- public string Message { get; private set; }
- ///
- /// Gets the exception.
- ///
- ///
- /// The exception.
- ///
- public Exception Exception { get; private set; }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/LogLevel.cs b/src/redmine-net-api/Logging/LogLevel.cs
new file mode 100644
index 00000000..a58e1500
--- /dev/null
+++ b/src/redmine-net-api/Logging/LogLevel.cs
@@ -0,0 +1,32 @@
+namespace Redmine.Net.Api.Logging;
+
+///
+/// Defines logging severity levels
+///
+public enum LogLevel
+{
+ ///
+ ///
+ ///
+ Trace,
+ ///
+ ///
+ ///
+ Debug,
+ ///
+ ///
+ ///
+ Information,
+ ///
+ ///
+ ///
+ Warning,
+ ///
+ ///
+ ///
+ Error,
+ ///
+ ///
+ ///
+ Critical
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/Logger.cs b/src/redmine-net-api/Logging/Logger.cs
deleted file mode 100755
index 895f3a8e..00000000
--- a/src/redmine-net-api/Logging/Logger.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- Copyright 2011 - 2019 Adrian Popescu.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-namespace Redmine.Net.Api.Logging
-{
- ///
- ///
- ///
- public static class Logger
- {
- private static readonly object locker = new object();
- private static ILogger logger;
-
- ///
- /// Gets the current ILogger.
- ///
- ///
- /// The current.
- ///
- public static ILogger Current
- {
- get { return logger ?? (logger = new ConsoleLogger()); }
- private set { logger = value; }
- }
-
- ///
- /// Uses the logger.
- ///
- /// The logger.
- public static void UseLogger(ILogger logger)
- {
- lock (locker)
- {
- Current = logger;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/LoggerExtensions.cs b/src/redmine-net-api/Logging/LoggerExtensions.cs
deleted file mode 100755
index 5a55d1e8..00000000
--- a/src/redmine-net-api/Logging/LoggerExtensions.cs
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- Copyright 2011 - 2019 Adrian Popescu.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Globalization;
-
-namespace Redmine.Net.Api.Logging
-{
- ///
- ///
- ///
- public static class LoggerExtensions
- {
- ///
- /// Debugs the specified message.
- ///
- /// The logger.
- /// The message.
- public static void Debug(this ILogger logger, string message)
- {
- logger.Log(new LogEntry(LoggingEventType.Debug, message));
- }
-
- ///
- /// Debugs the specified message.
- ///
- /// The logger.
- /// The message.
- /// The exception.
- public static void Debug(this ILogger logger, string message, Exception exception)
- {
- logger.Log(new LogEntry(LoggingEventType.Debug, message, exception));
- }
-
- ///
- /// Debugs the specified format provider.
- ///
- /// The logger.
- /// The format provider.
- /// The format.
- /// The arguments.
- public static void Debug(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args)
- {
- logger.Log(new LogEntry(LoggingEventType.Debug, string.Format(formatProvider, format, args)));
- }
-
- ///
- /// Debugs the specified format.
- ///
- /// The logger.
- /// The format.
- /// The arguments.
- public static void Debug(this ILogger logger, string format, params object[] args)
- {
- logger.Log(new LogEntry(LoggingEventType.Debug, string.Format(CultureInfo.CurrentCulture, format, args)));
- }
-
- ///
- /// Informations the specified message.
- ///
- /// The logger.
- /// The message.
- public static void Information(this ILogger logger, string message)
- {
- logger.Log(new LogEntry(LoggingEventType.Information, message));
- }
-
- ///
- /// Informations the specified format provider.
- ///
- /// The logger.
- /// The format provider.
- /// The format.
- /// The arguments.
- public static void Information(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args)
- {
- logger.Log(new LogEntry(LoggingEventType.Information, string.Format(formatProvider, format, args)));
- }
-
- ///
- /// Informations the specified format.
- ///
- /// The logger.
- /// The format.
- /// The arguments.
- public static void Information(this ILogger logger, string format, params object[] args)
- {
- logger.Log(new LogEntry(LoggingEventType.Information, string.Format(CultureInfo.CurrentCulture, format, args)));
- }
-
- ///
- /// Warnings the specified message.
- ///
- /// The logger.
- /// The message.
- public static void Warning(this ILogger logger, string message)
- {
- logger.Log(new LogEntry(LoggingEventType.Warning, message));
- }
-
- ///
- /// Warnings the specified format provider.
- ///
- /// The logger.
- /// The format provider.
- /// The format.
- /// The arguments.
- public static void Warning(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args)
- {
- logger.Log(new LogEntry(LoggingEventType.Warning, string.Format(formatProvider, format, args)));
- }
-
- ///
- /// Warnings the specified format.
- ///
- /// The logger.
- /// The format.
- /// The arguments.
- public static void Warning(this ILogger logger, string format, params object[] args)
- {
- logger.Log(new LogEntry(LoggingEventType.Warning, string.Format(CultureInfo.CurrentCulture, format, args)));
- }
-
- ///
- /// Errors the specified exception.
- ///
- /// The logger.
- /// The exception.
- public static void Error(this ILogger logger, Exception exception)
- {
- logger.Log(new LogEntry(LoggingEventType.Error, exception.Message, exception));
- }
-
- ///
- /// Errors the specified message.
- ///
- /// The logger.
- /// The message.
- /// The exception.
- public static void Error(this ILogger logger, string message, Exception exception)
- {
- logger.Log(new LogEntry(LoggingEventType.Error, message, exception));
- }
-
- ///
- /// Errors the specified format provider.
- ///
- /// The logger.
- /// The format provider.
- /// The format.
- /// The arguments.
- public static void Error(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args)
- {
- logger.Log(new LogEntry(LoggingEventType.Error, string.Format(formatProvider, format, args)));
- }
-
- ///
- /// Errors the specified format.
- ///
- /// The logger.
- /// The format.
- /// The arguments.
- public static void Error(this ILogger logger, string format, params object[] args)
- {
- logger.Log(new LogEntry(LoggingEventType.Error, string.Format(CultureInfo.CurrentCulture, format, args)));
- }
-
- ///
- /// Fatals the specified exception.
- ///
- /// The logger.
- /// The exception.
- public static void Fatal(this ILogger logger, Exception exception)
- {
- logger.Log(new LogEntry(LoggingEventType.Fatal, exception.Message, exception));
- }
-
- ///
- /// Fatals the specified message.
- ///
- /// The logger.
- /// The message.
- /// The exception.
- public static void Fatal(this ILogger logger, string message, Exception exception)
- {
- logger.Log(new LogEntry(LoggingEventType.Fatal, message, exception));
- }
-
- ///
- /// Fatals the specified format provider.
- ///
- /// The logger.
- /// The format provider.
- /// The format.
- /// The arguments.
- public static void Fatal(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args)
- {
- logger.Log(new LogEntry(LoggingEventType.Fatal, string.Format(formatProvider, format, args)));
- }
-
- ///
- /// Fatals the specified format.
- ///
- /// The logger.
- /// The format.
- /// The arguments.
- public static void Fatal(this ILogger logger, string format, params object[] args)
- {
- logger.Log(new LogEntry(LoggingEventType.Fatal, string.Format(CultureInfo.CurrentCulture, format, args)));
- }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/LoggingEventType.cs b/src/redmine-net-api/Logging/LoggingEventType.cs
deleted file mode 100755
index e539f868..00000000
--- a/src/redmine-net-api/Logging/LoggingEventType.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- Copyright 2011 - 2019 Adrian Popescu.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-namespace Redmine.Net.Api.Logging
-{
- ///
- ///
- ///
- public enum LoggingEventType
- {
- ///
- /// The debug
- ///
- Debug,
- ///
- /// The information
- ///
- Information,
- ///
- /// The warning
- ///
- Warning,
- ///
- /// The error
- ///
- Error,
- ///
- /// The fatal
- ///
- Fatal
- };
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/MicrosoftLoggerRedmineAdapter.cs b/src/redmine-net-api/Logging/MicrosoftLoggerRedmineAdapter.cs
new file mode 100644
index 00000000..1ac86ef6
--- /dev/null
+++ b/src/redmine-net-api/Logging/MicrosoftLoggerRedmineAdapter.cs
@@ -0,0 +1,92 @@
+#if NET462_OR_GREATER || NETCOREAPP
+using System;
+using System.Collections.Generic;
+
+namespace Redmine.Net.Api.Logging;
+
+///
+/// Adapter that converts Microsoft.Extensions.Logging.ILogger to IRedmineLogger
+///
+public class MicrosoftLoggerRedmineAdapter : IRedmineLogger
+{
+ private readonly Microsoft.Extensions.Logging.ILogger _microsoftLogger;
+
+ ///
+ /// Creates a new adapter for Microsoft.Extensions.Logging.ILogger
+ ///
+ /// The Microsoft logger to adapt
+ /// Thrown if microsoftLogger is null
+ public MicrosoftLoggerRedmineAdapter(Microsoft.Extensions.Logging.ILogger microsoftLogger)
+ {
+ _microsoftLogger = microsoftLogger ?? throw new ArgumentNullException(nameof(microsoftLogger));
+ }
+
+ ///
+ /// Checks if logging is enabled for the specified level
+ ///
+ public bool IsEnabled(LogLevel level)
+ {
+ return _microsoftLogger.IsEnabled(ToMicrosoftLogLevel(level));
+ }
+
+ ///
+ /// Logs a message with the specified level
+ ///
+ public void Log(LogLevel level, string message, Exception exception = null)
+ {
+ _microsoftLogger.Log(
+ ToMicrosoftLogLevel(level),
+ 0, // eventId
+ message,
+ exception,
+ (s, e) => s);
+ }
+
+ ///
+ /// Creates a scoped logger with additional context
+ ///
+ public IRedmineLogger CreateScope(string scopeName, IDictionary scopeProperties = null)
+ {
+ var scopeData = new Dictionary
+ {
+ ["ScopeName"] = scopeName
+ };
+
+ // Add additional properties if provided
+ if (scopeProperties != null)
+ {
+ foreach (var prop in scopeProperties)
+ {
+ scopeData[prop.Key] = prop.Value;
+ }
+ }
+
+ // Create a single scope with all properties
+ var disposableScope = _microsoftLogger.BeginScope(scopeData);
+
+ // Return a new adapter that will close the scope when disposed
+ return new ScopedMicrosoftLoggerAdapter(_microsoftLogger, disposableScope);
+ }
+
+ private class ScopedMicrosoftLoggerAdapter(Microsoft.Extensions.Logging.ILogger logger, IDisposable scope)
+ : MicrosoftLoggerRedmineAdapter(logger), IDisposable
+ {
+ public void Dispose()
+ {
+ scope?.Dispose();
+ }
+ }
+
+
+ private static Microsoft.Extensions.Logging.LogLevel ToMicrosoftLogLevel(LogLevel level) => level switch
+ {
+ LogLevel.Trace => Microsoft.Extensions.Logging.LogLevel.Trace,
+ LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug,
+ LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information,
+ LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning,
+ LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error,
+ LogLevel.Critical => Microsoft.Extensions.Logging.LogLevel.Critical,
+ _ => Microsoft.Extensions.Logging.LogLevel.Information
+ };
+}
+#endif
diff --git a/src/redmine-net-api/Logging/RedmineConsoleLogger.cs b/src/redmine-net-api/Logging/RedmineConsoleLogger.cs
new file mode 100644
index 00000000..23b03cea
--- /dev/null
+++ b/src/redmine-net-api/Logging/RedmineConsoleLogger.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+
+namespace Redmine.Net.Api.Logging;
+
+///
+///
+///
+///
+///
+///
+///
+///
+public class RedmineConsoleLogger(string categoryName = "Redmine", LogLevel minLevel = LogLevel.Information) : IRedmineLogger
+{
+ ///
+ ///
+ ///
+ ///
+ ///
+ public bool IsEnabled(LogLevel level) => level >= minLevel;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Log(LogLevel level, string message, Exception exception = null)
+ {
+ if (!IsEnabled(level))
+ {
+ return;
+ }
+
+ // var originalColor = Console.ForegroundColor;
+ //
+ // Console.ForegroundColor = level switch
+ // {
+ // LogLevel.Trace => ConsoleColor.Gray,
+ // LogLevel.Debug => ConsoleColor.Gray,
+ // LogLevel.Information => ConsoleColor.White,
+ // LogLevel.Warning => ConsoleColor.Yellow,
+ // LogLevel.Error => ConsoleColor.Red,
+ // LogLevel.Critical => ConsoleColor.Red,
+ // _ => ConsoleColor.White
+ // };
+
+ Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] [{categoryName}] {message}");
+
+ if (exception != null)
+ {
+ Console.WriteLine($"Exception: {exception}");
+ }
+
+ // Console.ForegroundColor = originalColor;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IRedmineLogger CreateScope(string scopeName, IDictionary scopeProperties = null)
+ {
+ return new RedmineConsoleLogger($"{categoryName}.{scopeName}", minLevel);
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/RedmineConsoleTraceListener.cs b/src/redmine-net-api/Logging/RedmineConsoleTraceListener.cs
deleted file mode 100644
index 531f4371..00000000
--- a/src/redmine-net-api/Logging/RedmineConsoleTraceListener.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- Copyright 2011 - 2019 Adrian Popescu.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Diagnostics;
-
-namespace Redmine.Net.Api.Logging
-{
- ///
- ///
- ///
- public sealed class RedmineConsoleTraceListener : TraceListener
- {
- #region implemented abstract members of TraceListener
-
- ///
- /// When overridden in a derived class, writes the specified message to the listener you create in the derived class.
- ///
- /// A message to write.
- public override void Write (string message)
- {
- Console.Write(message);
- }
-
- ///
- /// When overridden in a derived class, writes a message to the listener you create in the derived class, followed by a line terminator.
- ///
- /// A message to write.
- public override void WriteLine (string message)
- {
- Console.WriteLine(message);
- }
-
- #endregion
-
- ///
- /// Writes trace information, a message, and event information to the listener specific output.
- ///
- /// A object that contains the current process ID, thread ID, and stack trace information.
- /// A name used to identify the output, typically the name of the application that generated the trace event.
- /// One of the values specifying the type of event that has caused the trace.
- /// A numeric identifier for the event.
- /// A message to write.
- ///
- ///
- ///
- ///
- public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id,
- string message)
- {
- WriteLine(message);
- }
-
- ///
- /// Writes trace information, a formatted array of objects and event information to the listener specific output.
- ///
- /// A object that contains the current process ID, thread ID, and stack trace information.
- /// A name used to identify the output, typically the name of the application that generated the trace event.
- /// One of the values specifying the type of event that has caused the trace.
- /// A numeric identifier for the event.
- /// A format string that contains zero or more format items, which correspond to objects in the array.
- /// An object array containing zero or more objects to format.
- ///
- ///
- ///
- ///
- public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id,
- string format, params object[] args)
- {
- WriteLine(string.Format(format, args));
- }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/RedmineLoggerExtensions.cs b/src/redmine-net-api/Logging/RedmineLoggerExtensions.cs
new file mode 100644
index 00000000..39012412
--- /dev/null
+++ b/src/redmine-net-api/Logging/RedmineLoggerExtensions.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Diagnostics;
+#if !(NET20 || NET40)
+using System.Threading.Tasks;
+#endif
+namespace Redmine.Net.Api.Logging;
+
+///
+///
+///
+public static class RedmineLoggerExtensions
+{
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void Trace(this IRedmineLogger logger, string message, Exception exception = null)
+ => logger.Log(LogLevel.Trace, message, exception);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void Debug(this IRedmineLogger logger, string message, Exception exception = null)
+ => logger.Log(LogLevel.Debug, message, exception);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void Info(this IRedmineLogger logger, string message, Exception exception = null)
+ => logger.Log(LogLevel.Information, message, exception);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void Warn(this IRedmineLogger logger, string message, Exception exception = null)
+ => logger.Log(LogLevel.Warning, message, exception);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void Error(this IRedmineLogger logger, string message, Exception exception = null)
+ => logger.Log(LogLevel.Error, message, exception);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void Critical(this IRedmineLogger logger, string message, Exception exception = null)
+ => logger.Log(LogLevel.Critical, message, exception);
+
+#if !(NET20 || NET40)
+ ///
+ /// Creates and logs timing information for an operation
+ ///
+ public static async Task TimeOperationAsync(this IRedmineLogger logger, string operationName, Func> operation)
+ {
+ if (!logger.IsEnabled(LogLevel.Debug))
+ return await operation().ConfigureAwait(false);
+
+ var sw = Stopwatch.StartNew();
+ try
+ {
+ return await operation().ConfigureAwait(false);
+ }
+ finally
+ {
+ sw.Stop();
+ logger.Debug($"Operation '{operationName}' completed in {sw.ElapsedMilliseconds}ms");
+ }
+ }
+ #endif
+
+ ///
+ /// Creates and logs timing information for an operation
+ ///
+ public static T TimeOperationAsync(this IRedmineLogger logger, string operationName, Func operation)
+ {
+ if (!logger.IsEnabled(LogLevel.Debug))
+ return operation();
+
+ var sw = Stopwatch.StartNew();
+ try
+ {
+ return operation();
+ }
+ finally
+ {
+ sw.Stop();
+ logger.Debug($"Operation '{operationName}' completed in {sw.ElapsedMilliseconds}ms");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/RedmineLoggerFactory.cs b/src/redmine-net-api/Logging/RedmineLoggerFactory.cs
new file mode 100644
index 00000000..9b2dddff
--- /dev/null
+++ b/src/redmine-net-api/Logging/RedmineLoggerFactory.cs
@@ -0,0 +1,75 @@
+#if NET462_OR_GREATER || NETCOREAPP
+namespace Redmine.Net.Api.Logging;
+
+///
+///
+///
+public static class RedmineLoggerFactory
+{
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static Microsoft.Extensions.Logging.ILogger CreateMicrosoftLoggerAdapter(IRedmineLogger redmineLogger,
+ string categoryName = "Redmine")
+ {
+ if (redmineLogger == null || redmineLogger == RedmineNullLogger.Instance)
+ {
+ return Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
+ }
+
+ return new RedmineLoggerMicrosoftAdapter(redmineLogger, categoryName);
+ }
+
+ ///
+ /// Creates an adapter that exposes a Microsoft.Extensions.Logging.ILogger as IRedmineLogger
+ ///
+ /// The Microsoft logger to adapt
+ /// A Redmine logger implementation
+ public static IRedmineLogger CreateMicrosoftLogger(Microsoft.Extensions.Logging.ILogger microsoftLogger)
+ {
+ return microsoftLogger != null
+ ? new MicrosoftLoggerRedmineAdapter(microsoftLogger)
+ : RedmineNullLogger.Instance;
+ }
+
+ ///
+ /// Creates a logger that writes to the console
+ ///
+ public static IRedmineLogger CreateConsoleLogger(LogLevel minLevel = LogLevel.Information)
+ {
+ return new RedmineConsoleLogger(minLevel: minLevel);
+ }
+
+ // ///
+ // /// Creates an adapter for Serilog
+ // ///
+ // public static IRedmineLogger CreateSerilogAdapter(Serilog.ILogger logger)
+ // {
+ // if (logger == null) return NullRedmineLogger.Instance;
+ // return new SerilogAdapter(logger);
+ // }
+ //
+ // ///
+ // /// Creates an adapter for NLog
+ // ///
+ // public static IRedmineLogger CreateNLogAdapter(NLog.ILogger logger)
+ // {
+ // if (logger == null) return NullRedmineLogger.Instance;
+ // return new NLogAdapter(logger);
+ // }
+ //
+ // ///
+ // /// Creates an adapter for log4net
+ // ///
+ // public static IRedmineLogger CreateLog4NetAdapter(log4net.ILog logger)
+ // {
+ // if (logger == null) return NullRedmineLogger.Instance;
+ // return new Log4NetAdapter(logger);
+ // }
+}
+
+
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/RedmineLoggerMicrosoftAdapter.cs b/src/redmine-net-api/Logging/RedmineLoggerMicrosoftAdapter.cs
new file mode 100644
index 00000000..56d152f9
--- /dev/null
+++ b/src/redmine-net-api/Logging/RedmineLoggerMicrosoftAdapter.cs
@@ -0,0 +1,97 @@
+#if NET462_OR_GREATER || NETCOREAPP
+using System;
+using System.Collections.Generic;
+
+namespace Redmine.Net.Api.Logging;
+
+///
+///
+///
+public class RedmineLoggerMicrosoftAdapter : Microsoft.Extensions.Logging.ILogger
+{
+ private readonly IRedmineLogger _redmineLogger;
+ private readonly string _categoryName;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedmineLoggerMicrosoftAdapter(IRedmineLogger redmineLogger, string categoryName = "Redmine.Net.Api")
+ {
+ _redmineLogger = redmineLogger ?? throw new ArgumentNullException(nameof(redmineLogger));
+ _categoryName = categoryName;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IDisposable BeginScope(TState state)
+ {
+ if (state is IDictionary dict)
+ {
+ _redmineLogger.CreateScope("Scope", dict);
+ }
+ else
+ {
+ var scopeName = state?.ToString() ?? "Scope";
+ _redmineLogger.CreateScope(scopeName);
+ }
+
+ return new NoOpDisposable();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel)
+ {
+ return _redmineLogger.IsEnabled(ToRedmineLogLevel(logLevel));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Log(
+ Microsoft.Extensions.Logging.LogLevel logLevel,
+ Microsoft.Extensions.Logging.EventId eventId,
+ TState state,
+ Exception exception,
+ Func formatter)
+ {
+ if (!IsEnabled(logLevel))
+ return;
+
+ var message = formatter(state, exception);
+ _redmineLogger.Log(ToRedmineLogLevel(logLevel), message, exception);
+ }
+
+ private static LogLevel ToRedmineLogLevel(Microsoft.Extensions.Logging.LogLevel level) => level switch
+ {
+ Microsoft.Extensions.Logging.LogLevel.Trace => LogLevel.Trace,
+ Microsoft.Extensions.Logging.LogLevel.Debug => LogLevel.Debug,
+ Microsoft.Extensions.Logging.LogLevel.Information => LogLevel.Information,
+ Microsoft.Extensions.Logging.LogLevel.Warning => LogLevel.Warning,
+ Microsoft.Extensions.Logging.LogLevel.Error => LogLevel.Error,
+ Microsoft.Extensions.Logging.LogLevel.Critical => LogLevel.Critical,
+ _ => LogLevel.Information
+ };
+
+ private class NoOpDisposable : IDisposable
+ {
+ public void Dispose() { }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/RedmineLoggingOptions.cs b/src/redmine-net-api/Logging/RedmineLoggingOptions.cs
new file mode 100644
index 00000000..d7b510d4
--- /dev/null
+++ b/src/redmine-net-api/Logging/RedmineLoggingOptions.cs
@@ -0,0 +1,22 @@
+namespace Redmine.Net.Api.Logging;
+
+///
+/// Options for configuring Redmine logging
+///
+public sealed class RedmineLoggingOptions
+{
+ ///
+ /// Gets or sets the minimum log level. The default value is LogLevel.Information
+ ///
+ public LogLevel MinimumLevel { get; set; } = LogLevel.Information;
+
+ ///
+ /// Gets or sets whether to include HTTP request/response details in logs
+ ///
+ public bool IncludeHttpDetails { get; set; }
+
+ ///
+ /// Gets or sets whether performance metrics should be logged
+ ///
+ public bool LogPerformanceMetrics { get; set; }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/RedmineNullLogger.cs b/src/redmine-net-api/Logging/RedmineNullLogger.cs
new file mode 100644
index 00000000..0d47ab1d
--- /dev/null
+++ b/src/redmine-net-api/Logging/RedmineNullLogger.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+
+namespace Redmine.Net.Api.Logging;
+
+///
+///
+///
+public class RedmineNullLogger : IRedmineLogger
+{
+ ///
+ ///
+ ///
+ public static readonly RedmineNullLogger Instance = new RedmineNullLogger();
+
+ private RedmineNullLogger() { }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public bool IsEnabled(LogLevel level) => false;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Log(LogLevel level, string message, Exception exception = null) { }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IRedmineLogger CreateScope(string scopeName, IDictionary scopeProperties = null) => this;
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Logging/TraceLogger.cs b/src/redmine-net-api/Logging/TraceLogger.cs
deleted file mode 100644
index 324252dc..00000000
--- a/src/redmine-net-api/Logging/TraceLogger.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- Copyright 2011 - 2019 Adrian Popescu.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Diagnostics;
-
-namespace Redmine.Net.Api.Logging
-{
- ///
- ///
- ///
- public sealed class TraceLogger : ILogger
- {
- ///
- /// Logs the specified entry.
- ///
- /// The entry.
- ///
- public void Log(LogEntry entry)
- {
- switch (entry.Severity)
- {
- case LoggingEventType.Debug:
- Trace.WriteLine(entry.Message, "Debug");
- break;
- case LoggingEventType.Information:
- Trace.TraceInformation(entry.Message);
- break;
- case LoggingEventType.Warning:
- Trace.TraceWarning(entry.Message);
- break;
- case LoggingEventType.Error:
- Trace.TraceError(entry.Message);
- break;
- case LoggingEventType.Fatal:
- Trace.WriteLine(entry.Message, "Fatal");
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/ApiRequestMessageContent.cs b/src/redmine-net-api/Net/ApiRequestMessageContent.cs
deleted file mode 100644
index 94c5f5e9..00000000
--- a/src/redmine-net-api/Net/ApiRequestMessageContent.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-namespace Redmine.Net.Api.Net;
-
-internal abstract class ApiRequestMessageContent
-{
- public string ContentType { get; internal set; }
-
- public byte[] Body { get; internal set; }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs b/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs
deleted file mode 100644
index f039a451..00000000
--- a/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System.Collections.Generic;
-using System.Text;
-using Redmine.Net.Api.Serialization;
-
-namespace Redmine.Net.Api.Net;
-
-internal static class ApiResponseMessageExtensions
-{
- internal static T DeserializeTo(this ApiResponseMessage responseMessage, IRedmineSerializer redmineSerializer) where T : new()
- {
- if (responseMessage?.Content == null)
- {
- return default;
- }
-
- var responseAsString = Encoding.UTF8.GetString(responseMessage.Content);
-
- return redmineSerializer.Deserialize(responseAsString);
- }
-
- internal static PagedResults DeserializeToPagedResults(this ApiResponseMessage responseMessage, IRedmineSerializer redmineSerializer) where T : class, new()
- {
- if (responseMessage?.Content == null)
- {
- return default;
- }
-
- var responseAsString = Encoding.UTF8.GetString(responseMessage.Content);
-
- return redmineSerializer.DeserializeToPagedResults(responseAsString);
- }
-
- internal static List DeserializeToList(this ApiResponseMessage responseMessage, IRedmineSerializer redmineSerializer) where T : class, new()
- {
- if (responseMessage?.Content == null)
- {
- return default;
- }
-
- var responseAsString = Encoding.UTF8.GetString(responseMessage.Content);
-
- return redmineSerializer.Deserialize>(responseAsString);
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/HttpVerbs.cs b/src/redmine-net-api/Net/HttpVerbs.cs
deleted file mode 100644
index e7851896..00000000
--- a/src/redmine-net-api/Net/HttpVerbs.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
-Copyright 2011 - 2025 Adrian Popescu
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-namespace Redmine.Net.Api
-{
-
- ///
- ///
- ///
- public static class HttpVerbs
- {
- ///
- /// Represents an HTTP GET protocol method that is used to get an entity identified by a URI.
- ///
- public const string GET = "GET";
- ///
- /// Represents an HTTP PUT protocol method that is used to replace an entity identified by a URI.
- ///
- public const string PUT = "PUT";
- ///
- /// Represents an HTTP POST protocol method that is used to post a new entity as an addition to a URI.
- ///
- public const string POST = "POST";
- ///
- /// Represents an HTTP PATCH protocol method that is used to patch an existing entity identified by a URI.
- ///
- public const string PATCH = "PATCH";
- ///
- /// Represents an HTTP DELETE protocol method that is used to delete an existing entity identified by a URI.
- ///
- public const string DELETE = "DELETE";
-
-
- internal const string DOWNLOAD = "DOWNLOAD";
-
- internal const string UPLOAD = "UPLOAD";
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/IRedmineApiClient.cs b/src/redmine-net-api/Net/IRedmineApiClient.cs
deleted file mode 100644
index f9ffc4f8..00000000
--- a/src/redmine-net-api/Net/IRedmineApiClient.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System.Threading;
-#if!(NET20)
-using System.Threading.Tasks;
-#endif
-
-namespace Redmine.Net.Api.Net;
-
-///
-///
-///
-internal interface IRedmineApiClient
-{
- ApiResponseMessage Get(string address, RequestOptions requestOptions = null);
- ApiResponseMessage GetPaged(string address, RequestOptions requestOptions = null);
- ApiResponseMessage Create(string address, string payload, RequestOptions requestOptions = null);
- ApiResponseMessage Update(string address, string payload, RequestOptions requestOptions = null);
- ApiResponseMessage Patch(string address, string payload, RequestOptions requestOptions = null);
- ApiResponseMessage Delete(string address, RequestOptions requestOptions = null);
- ApiResponseMessage Upload(string address, byte[] data, RequestOptions requestOptions = null);
- ApiResponseMessage Download(string address, RequestOptions requestOptions = null);
-
- #if !(NET20)
- Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
- Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
- Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
- Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
- Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
- Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
- Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
- Task DownloadAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default);
- #endif
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs b/src/redmine-net-api/Net/IRedmineApiClientOptions.cs
deleted file mode 100644
index 3a11601f..00000000
--- a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.Net.Cache;
-using System.Net.Security;
-using System.Security.Cryptography.X509Certificates;
-
-namespace Redmine.Net.Api.Net
-{
- ///
- ///
- ///
- public interface IRedmineApiClientOptions
- {
- ///
- ///
- ///
- bool? AutoRedirect { get; set; }
-
- ///
- ///
- ///
- CookieContainer CookieContainer { get; set; }
-
- ///
- ///
- ///
- DecompressionMethods? DecompressionFormat { get; set; }
-
- ///
- ///
- ///
- ICredentials Credentials { get; set; }
-
- ///
- ///
- ///
- Dictionary DefaultHeaders { get; set; }
-
- ///
- ///
- ///
- IWebProxy Proxy { get; set; }
-
- ///
- ///
- ///
- bool? KeepAlive { get; set; }
-
- ///
- ///
- ///
- int? MaxAutomaticRedirections { get; set; }
-
- ///
- ///
- ///
- long? MaxRequestContentBufferSize { get; set; }
-
- ///
- ///
- ///
- long? MaxResponseContentBufferSize { get; set; }
-
- ///
- ///
- ///
- int? MaxConnectionsPerServer { get; set; }
-
- ///
- ///
- ///
- int? MaxResponseHeadersLength { get; set; }
-
- ///
- ///
- ///
- bool? PreAuthenticate { get; set; }
-
- ///
- ///
- ///
- RequestCachePolicy RequestCachePolicy { get; set; }
-
- ///
- ///
- ///
- string Scheme { get; set; }
-
- ///
- ///
- ///
- RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; }
-
- ///
- ///
- ///
- TimeSpan? Timeout { get; set; }
-
- ///
- ///
- ///
- bool? UnsafeAuthenticatedConnectionSharing { get; set; }
-
- ///
- ///
- ///
- string UserAgent { get; set; }
-
- ///
- ///
- ///
- bool? UseCookies { get; set; }
-
- ///
- ///
- ///
- bool? UseDefaultCredentials { get; set; }
-
- ///
- ///
- ///
- bool? UseProxy { get; set; }
-
- ///
- ///
- ///
- /// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported.
- Version ProtocolVersion { get; set; }
-
- ///
- ///
- ///
- bool CheckCertificateRevocationList { get; set; }
-
- ///
- ///
- ///
- int? DefaultConnectionLimit { get; set; }
-
- ///
- ///
- ///
- int? DnsRefreshTimeout { get; set; }
-
- ///
- ///
- ///
- bool? EnableDnsRoundRobin { get; set; }
-
- ///
- ///
- ///
- int? MaxServicePoints { get; set; }
-
- ///
- ///
- ///
- int? MaxServicePointIdleTime { get; set; }
-
- ///
- ///
- ///
- SecurityProtocolType? SecurityProtocolType { get; set; }
-
- #if NET40_OR_GREATER || NETCOREAPP
- ///
- ///
- ///
- public X509CertificateCollection ClientCertificates { get; set; }
- #endif
-
- #if(NET46_OR_GREATER || NETCOREAPP)
- ///
- ///
- ///
- public bool? ReusePort { get; set; }
- #endif
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/RedmineApiUrls.cs b/src/redmine-net-api/Net/Internal/RedmineApiUrls.cs
similarity index 97%
rename from src/redmine-net-api/Net/RedmineApiUrls.cs
rename to src/redmine-net-api/Net/Internal/RedmineApiUrls.cs
index 54a9d452..8fa0b3a1 100644
--- a/src/redmine-net-api/Net/RedmineApiUrls.cs
+++ b/src/redmine-net-api/Net/Internal/RedmineApiUrls.cs
@@ -18,10 +18,11 @@ limitations under the License.
using System.Collections.Generic;
using Redmine.Net.Api.Exceptions;
using Redmine.Net.Api.Extensions;
+using Redmine.Net.Api.Http;
using Redmine.Net.Api.Types;
using Version = Redmine.Net.Api.Types.Version;
-namespace Redmine.Net.Api.Net
+namespace Redmine.Net.Api.Net.Internal
{
internal sealed class RedmineApiUrls
{
@@ -31,6 +32,7 @@ internal sealed class RedmineApiUrls
{
{typeof(Attachment), RedmineKeys.ATTACHMENTS},
{typeof(CustomField), RedmineKeys.CUSTOM_FIELDS},
+ {typeof(DocumentCategory), RedmineKeys.ENUMERATION_DOCUMENT_CATEGORIES},
{typeof(Group), RedmineKeys.GROUPS},
{typeof(Issue), RedmineKeys.ISSUES},
{typeof(IssueCategory), RedmineKeys.ISSUE_CATEGORIES},
@@ -150,7 +152,7 @@ internal string CreateEntityFragment(Type type, string ownerId = null)
if (type == typeof(Upload))
{
- return $"{RedmineKeys.UPLOADS}.{Format}";
+ return UploadFragment(ownerId); //$"{RedmineKeys.UPLOADS}.{Format}";
}
if (type == typeof(Attachment) || type == typeof(Attachments))
diff --git a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs b/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs
similarity index 77%
rename from src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs
rename to src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs
index 4312ba9e..753b439d 100644
--- a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs
+++ b/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs
@@ -14,10 +14,7 @@ You may obtain a copy of the License at
limitations under the License.
*/
-using Redmine.Net.Api.Exceptions;
-using Redmine.Net.Api.Extensions;
-
-namespace Redmine.Net.Api.Net;
+namespace Redmine.Net.Api.Net.Internal;
internal static class RedmineApiUrlsExtensions
{
@@ -63,21 +60,11 @@ public static string ProjectRepositoryRemoveRelatedIssue(this RedmineApiUrls red
public static string ProjectNews(this RedmineApiUrls redmineApiUrls, string projectIdentifier)
{
- if (projectIdentifier.IsNullOrWhiteSpace())
- {
- throw new RedmineException($"Argument '{nameof(projectIdentifier)}' is null or whitespace");
- }
-
- return $"{RedmineKeys.PROJECT}/{projectIdentifier}/{RedmineKeys.NEWS}.{redmineApiUrls.Format}";
+ return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.NEWS}.{redmineApiUrls.Format}";
}
public static string ProjectMemberships(this RedmineApiUrls redmineApiUrls, string projectIdentifier)
{
- if (projectIdentifier.IsNullOrWhiteSpace())
- {
- throw new RedmineException($"Argument '{nameof(projectIdentifier)}' is null or whitespace");
- }
-
return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.MEMBERSHIPS}.{redmineApiUrls.Format}";
}
@@ -118,61 +105,26 @@ public static string ProjectWikis(this RedmineApiUrls redmineApiUrls, string pro
public static string IssueWatcherAdd(this RedmineApiUrls redmineApiUrls, string issueIdentifier)
{
- if (issueIdentifier.IsNullOrWhiteSpace())
- {
- throw new RedmineException($"Argument '{nameof(issueIdentifier)}' is null or whitespace");
- }
-
return $"{RedmineKeys.ISSUES}/{issueIdentifier}/{RedmineKeys.WATCHERS}.{redmineApiUrls.Format}";
}
public static string IssueWatcherRemove(this RedmineApiUrls redmineApiUrls, string issueIdentifier, string userId)
{
- if (issueIdentifier.IsNullOrWhiteSpace())
- {
- throw new RedmineException($"Argument '{nameof(issueIdentifier)}' is null or whitespace");
- }
-
- if (userId.IsNullOrWhiteSpace())
- {
- throw new RedmineException($"Argument '{nameof(userId)}' is null or whitespace");
- }
-
return $"{RedmineKeys.ISSUES}/{issueIdentifier}/{RedmineKeys.WATCHERS}/{userId}.{redmineApiUrls.Format}";
}
public static string GroupUserAdd(this RedmineApiUrls redmineApiUrls, string groupIdentifier)
{
- if (groupIdentifier.IsNullOrWhiteSpace())
- {
- throw new RedmineException($"Argument '{nameof(groupIdentifier)}' is null or whitespace");
- }
-
return $"{RedmineKeys.GROUPS}/{groupIdentifier}/{RedmineKeys.USERS}.{redmineApiUrls.Format}";
}
public static string GroupUserRemove(this RedmineApiUrls redmineApiUrls, string groupIdentifier, string userId)
{
- if (groupIdentifier.IsNullOrWhiteSpace())
- {
- throw new RedmineException($"Argument '{nameof(groupIdentifier)}' is null or whitespace");
- }
-
- if (userId.IsNullOrWhiteSpace())
- {
- throw new RedmineException($"Argument '{nameof(userId)}' is null or whitespace");
- }
-
return $"{RedmineKeys.GROUPS}/{groupIdentifier}/{RedmineKeys.USERS}/{userId}.{redmineApiUrls.Format}";
}
public static string AttachmentUpdate(this RedmineApiUrls redmineApiUrls, string issueIdentifier)
{
- if (issueIdentifier.IsNullOrWhiteSpace())
- {
- throw new RedmineException($"Argument '{nameof(issueIdentifier)}' is null or whitespace");
- }
-
return $"{RedmineKeys.ATTACHMENTS}/{RedmineKeys.ISSUES}/{issueIdentifier}.{redmineApiUrls.Format}";
}
diff --git a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs
deleted file mode 100644
index 6d573e17..00000000
--- a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System.Collections.Specialized;
-using System.Globalization;
-using System.Text;
-
-namespace Redmine.Net.Api.Extensions
-{
- ///
- ///
- ///
- public static class NameValueCollectionExtensions
- {
- ///
- /// Gets the parameter value.
- ///
- /// The parameters.
- /// Name of the parameter.
- ///
- public static string GetParameterValue(this NameValueCollection parameters, string parameterName)
- {
- return GetValue(parameters, parameterName);
- }
-
- ///
- /// Gets the parameter value.
- ///
- /// The parameters.
- /// Name of the parameter.
- ///
- public static string GetValue(this NameValueCollection parameters, string key)
- {
- if (parameters == null)
- {
- return null;
- }
-
- var value = parameters.Get(key);
-
- return value.IsNullOrWhiteSpace() ? null : value;
- }
-
- ///
- ///
- ///
- ///
- ///
- public static string ToQueryString(this NameValueCollection requestParameters)
- {
- if (requestParameters == null || requestParameters.Count == 0)
- {
- return null;
- }
-
- var delimiter = string.Empty;
-
- var stringBuilder = new StringBuilder();
-
- for (var index = 0; index < requestParameters.Count; ++index)
- {
- stringBuilder
- .Append(delimiter)
- .Append(requestParameters.AllKeys[index].ToString(CultureInfo.InvariantCulture))
- .Append('=')
- .Append(requestParameters[index].ToString(CultureInfo.InvariantCulture));
- delimiter = "&";
- }
-
- var queryString = stringBuilder.ToString();
-
- stringBuilder.Length = 0;
-
- return queryString;
- }
-
- internal static NameValueCollection AddPagingParameters(this NameValueCollection parameters, int pageSize, int offset)
- {
- parameters ??= new NameValueCollection();
-
- if(pageSize <= 0)
- {
- pageSize = RedmineConstants.DEFAULT_PAGE_SIZE_VALUE;
- }
-
- if(offset < 0)
- {
- offset = 0;
- }
-
- parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture));
- parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture));
-
- return parameters;
- }
-
- internal static NameValueCollection AddParamsIfExist(this NameValueCollection parameters, string[] include)
- {
- if (include is not {Length: > 0})
- {
- return parameters;
- }
-
- parameters ??= new NameValueCollection();
-
- parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include));
-
- return parameters;
- }
-
- internal static void AddIfNotNull(this NameValueCollection nameValueCollection, string key, string value)
- {
- if (!value.IsNullOrWhiteSpace())
- {
- nameValueCollection.Add(key, value);
- }
- }
-
- internal static void AddIfNotNull(this NameValueCollection nameValueCollection, string key, bool? value)
- {
- if (value.HasValue)
- {
- nameValueCollection.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs
deleted file mode 100644
index 3a3b1a2a..00000000
--- a/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Text;
-using Redmine.Net.Api.Exceptions;
-using Redmine.Net.Api.Serialization;
-using Redmine.Net.Api.Types;
-
-namespace Redmine.Net.Api.Extensions
-{
- ///
- ///
- ///
- internal static class WebExceptionExtensions
- {
- ///
- /// Handles the web exception.
- ///
- /// The exception.
- ///
- /// Timeout!
- /// Bad domain name!
- ///
- ///
- ///
- ///
- /// The page that you are trying to update is staled!
- ///
- ///
- ///
- public static void HandleWebException(this WebException exception, IRedmineSerializer serializer)
- {
- if (exception == null)
- {
- return;
- }
-
- var innerException = exception.InnerException ?? exception;
-
- switch (exception.Status)
- {
- case WebExceptionStatus.Timeout:
- throw new RedmineTimeoutException(nameof(WebExceptionStatus.Timeout), innerException);
- case WebExceptionStatus.NameResolutionFailure:
- throw new NameResolutionFailureException("Bad domain name.", innerException);
-
- case WebExceptionStatus.ProtocolError:
- {
- var response = (HttpWebResponse)exception.Response;
- switch ((int)response.StatusCode)
- {
- case (int)HttpStatusCode.NotFound:
- throw new NotFoundException(response.StatusDescription, innerException);
-
- case (int)HttpStatusCode.Unauthorized:
- throw new UnauthorizedException(response.StatusDescription, innerException);
-
- case (int)HttpStatusCode.Forbidden:
- throw new ForbiddenException(response.StatusDescription, innerException);
-
- case (int)HttpStatusCode.Conflict:
- throw new ConflictException("The page that you are trying to update is staled!", innerException);
-
- case 422:
- RedmineException redmineException;
- var errors = GetRedmineExceptions(exception.Response, serializer);
-
- if (errors != null)
- {
- var sb = new StringBuilder();
- foreach (var error in errors)
- {
- sb.Append(error.Info).Append(Environment.NewLine);
- }
-
- redmineException = new RedmineException($"Invalid or missing attribute parameters: {sb}", innerException, "Unprocessable Content");
- sb.Length = 0;
- }
- else
- {
- redmineException = new RedmineException("Invalid or missing attribute parameters", innerException);
- }
-
- throw redmineException;
-
- case (int)HttpStatusCode.NotAcceptable:
- throw new NotAcceptableException(response.StatusDescription, innerException);
-
- default:
- throw new RedmineException(response.StatusDescription, innerException);
- }
- }
-
- default:
- throw new RedmineException(exception.Message, innerException);
- }
- }
-
- ///
- /// Gets the redmine exceptions.
- ///
- /// The web response.
- ///
- ///
- private static IEnumerable GetRedmineExceptions(this WebResponse webResponse, IRedmineSerializer serializer)
- {
- using (var responseStream = webResponse.GetResponseStream())
- {
- if (responseStream == null)
- {
- return null;
- }
-
- using (var streamReader = new StreamReader(responseStream))
- {
- var responseContent = streamReader.ReadToEnd();
-
- if (responseContent.IsNullOrWhiteSpace())
- {
- return null;
- }
-
- var result = serializer.DeserializeToPagedResults(responseContent);
- return result.Items;
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs b/src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs
deleted file mode 100644
index 3b7c1bd9..00000000
--- a/src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Collections.Specialized;
-using System.Net;
-using System.Net.Cache;
-
-namespace Redmine.Net.Api.Types
-{
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public interface IRedmineWebClient
- {
- ///
- ///
- ///
- string UserAgent { get; set; }
-
- ///
- ///
- ///
- bool UseProxy { get; set; }
-
- ///
- ///
- ///
- bool UseCookies { get; set; }
-
- ///
- ///
- ///
- TimeSpan? Timeout { get; set; }
-
- ///
- ///
- ///
- CookieContainer CookieContainer { get; set; }
-
- ///
- ///
- ///
- bool PreAuthenticate { get; set; }
-
- ///
- ///
- ///
- bool KeepAlive { get; set; }
-
- ///
- ///
- ///
- NameValueCollection QueryString { get; }
-
- ///
- ///
- ///
- bool UseDefaultCredentials { get; set; }
-
- ///
- ///
- ///
- ICredentials Credentials { get; set; }
-
- ///
- ///
- ///
- IWebProxy Proxy { get; set; }
-
- ///
- ///
- ///
- RequestCachePolicy CachePolicy { get; set; }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs b/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs
deleted file mode 100644
index 809d9afb..00000000
--- a/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Net.Cache;
-using System.Net.Security;
-using System.Security.Cryptography.X509Certificates;
-
-namespace Redmine.Net.Api.Net.WebClient;
-
-///
-///
-///
-public interface IRedmineWebClientOptions : IRedmineApiClientOptions
-{
-#if NET40_OR_GREATER || NETCOREAPP
- ///
- ///
- ///
- public X509CertificateCollection ClientCertificates { get; set; }
-#endif
-
- ///
- ///
- ///
- int? DefaultConnectionLimit { get; set; }
-
- ///
- ///
- ///
- Dictionary DefaultHeaders { get; set; }
-
- ///
- ///
- ///
- int? DnsRefreshTimeout { get; set; }
-
- ///
- ///
- ///
- bool? EnableDnsRoundRobin { get; set; }
-
- ///
- ///
- ///
- bool? KeepAlive { get; set; }
-
- ///
- ///
- ///
- int? MaxServicePoints { get; set; }
-
- ///
- ///
- ///
- int? MaxServicePointIdleTime { get; set; }
-
- ///
- ///
- ///
- RequestCachePolicy RequestCachePolicy { get; set; }
-
-#if(NET46_OR_GREATER || NETCOREAPP)
- ///
- ///
- ///
- public bool? ReusePort { get; set; }
-#endif
-
- ///
- ///
- ///
- RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; }
-
- ///
- ///
- ///
- bool? UnsafeAuthenticatedConnectionSharing { get; set; }
-
- ///
- ///
- ///
- /// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported.
- Version ProtocolVersion { get; set; }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs
deleted file mode 100644
index df94c7fa..00000000
--- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs
+++ /dev/null
@@ -1,362 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Collections.Specialized;
-using System.Net;
-using System.Text;
-using System.Threading;
-using Redmine.Net.Api.Authentication;
-#if!(NET20)
-using System.Threading.Tasks;
-#endif
-using Redmine.Net.Api.Extensions;
-using Redmine.Net.Api.Net.WebClient.MessageContent;
-using Redmine.Net.Api.Serialization;
-
-namespace Redmine.Net.Api.Net.WebClient
-{
- ///
- ///
- ///
- internal sealed class InternalRedmineApiWebClient : IRedmineApiClient
- {
- private static readonly byte[] EmptyBytes = Encoding.UTF8.GetBytes(string.Empty);
- private readonly Func _webClientFunc;
- private readonly IRedmineAuthentication _credentials;
- private readonly IRedmineSerializer _serializer;
-
- public InternalRedmineApiWebClient(RedmineManagerOptions redmineManagerOptions)
- : this(() => new InternalWebClient(redmineManagerOptions), redmineManagerOptions.Authentication, redmineManagerOptions.Serializer)
- {
- ConfigureServicePointManager(redmineManagerOptions.WebClientOptions);
- }
-
- public InternalRedmineApiWebClient(
- Func webClientFunc,
- IRedmineAuthentication authentication,
- IRedmineSerializer serializer)
- {
- _webClientFunc = webClientFunc;
- _credentials = authentication;
- _serializer = serializer;
- }
-
- private static void ConfigureServicePointManager(IRedmineWebClientOptions webClientOptions)
- {
- if (webClientOptions == null)
- {
- return;
- }
-
- if (webClientOptions.MaxServicePoints.HasValue)
- {
- ServicePointManager.MaxServicePoints = webClientOptions.MaxServicePoints.Value;
- }
-
- if (webClientOptions.MaxServicePointIdleTime.HasValue)
- {
- ServicePointManager.MaxServicePointIdleTime = webClientOptions.MaxServicePointIdleTime.Value;
- }
-
- ServicePointManager.SecurityProtocol = webClientOptions.SecurityProtocolType ?? ServicePointManager.SecurityProtocol;
-
- if (webClientOptions.DefaultConnectionLimit.HasValue)
- {
- ServicePointManager.DefaultConnectionLimit = webClientOptions.DefaultConnectionLimit.Value;
- }
-
- if (webClientOptions.DnsRefreshTimeout.HasValue)
- {
- ServicePointManager.DnsRefreshTimeout = webClientOptions.DnsRefreshTimeout.Value;
- }
-
- ServicePointManager.CheckCertificateRevocationList = webClientOptions.CheckCertificateRevocationList;
-
- if (webClientOptions.EnableDnsRoundRobin.HasValue)
- {
- ServicePointManager.EnableDnsRoundRobin = webClientOptions.EnableDnsRoundRobin.Value;
- }
-
- #if(NET46_OR_GREATER || NETCOREAPP)
- if (webClientOptions.ReusePort.HasValue)
- {
- ServicePointManager.ReusePort = webClientOptions.ReusePort.Value;
- }
- #endif
- }
-
- public ApiResponseMessage Get(string address, RequestOptions requestOptions = null)
- {
- return HandleRequest(address, HttpVerbs.GET, requestOptions);
- }
-
- public ApiResponseMessage GetPaged(string address, RequestOptions requestOptions = null)
- {
- return Get(address, requestOptions);
- }
-
- public ApiResponseMessage Create(string address, string payload, RequestOptions requestOptions = null)
- {
- var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer));
- return HandleRequest(address, HttpVerbs.POST, requestOptions, content);
- }
-
- public ApiResponseMessage Update(string address, string payload, RequestOptions requestOptions = null)
- {
- var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer));
- return HandleRequest(address, HttpVerbs.PUT, requestOptions, content);
- }
-
- public ApiResponseMessage Patch(string address, string payload, RequestOptions requestOptions = null)
- {
- var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer));
- return HandleRequest(address, HttpVerbs.PATCH, requestOptions, content);
- }
-
- public ApiResponseMessage Delete(string address, RequestOptions requestOptions = null)
- {
- return HandleRequest(address, HttpVerbs.DELETE, requestOptions);
- }
-
- public ApiResponseMessage Download(string address, RequestOptions requestOptions = null)
- {
- return HandleRequest(address, HttpVerbs.DOWNLOAD, requestOptions);
- }
-
- public ApiResponseMessage Upload(string address, byte[] data, RequestOptions requestOptions = null)
- {
- var content = new StreamApiRequestMessageContent(data);
- return HandleRequest(address, HttpVerbs.POST, requestOptions, content);
- }
-
- #if !(NET20)
- public async Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
- {
- return await HandleRequestAsync(address, HttpVerbs.GET, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false);
- }
-
- public Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
- {
- return GetAsync(address, requestOptions, cancellationToken);
- }
-
- public async Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
- {
- var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer));
- return await HandleRequestAsync(address, HttpVerbs.POST, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false);
- }
-
- public async Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
- {
- var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer));
- return await HandleRequestAsync(address, HttpVerbs.PUT, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false);
- }
-
- public async Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
- {
- var content = new StreamApiRequestMessageContent(data);
- return await HandleRequestAsync(address, HttpVerbs.POST, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false);
- }
-
- public async Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
- {
- var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer));
- return await HandleRequestAsync(address, HttpVerbs.PATCH, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false);
- }
-
- public async Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
- {
- return await HandleRequestAsync(address, HttpVerbs.DELETE, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false);
- }
-
- public async Task DownloadAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
- {
- return await HandleRequestAsync(address, HttpVerbs.DOWNLOAD, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false);
- }
-
- private Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null, CancellationToken cancellationToken = default)
- {
- return SendAsync(CreateRequestMessage(address, verb, requestOptions, content), cancellationToken);
- }
-
- private async Task SendAsync(ApiRequestMessage requestMessage, CancellationToken cancellationToken)
- {
- System.Net.WebClient webClient = null;
- byte[] response = null;
- NameValueCollection responseHeaders = null;
- try
- {
- webClient = _webClientFunc();
-
- cancellationToken.Register(webClient.CancelAsync);
-
- SetWebClientHeaders(webClient, requestMessage);
-
- if(IsGetOrDownload(requestMessage.Method))
- {
- response = await webClient.DownloadDataTaskAsync(requestMessage.RequestUri).ConfigureAwait(false);
- }
- else
- {
- byte[] payload;
- if (requestMessage.Content != null)
- {
- webClient.Headers.Add(HttpRequestHeader.ContentType, requestMessage.Content.ContentType);
- payload = requestMessage.Content.Body;
- }
- else
- {
- payload = EmptyBytes;
- }
-
- response = await webClient.UploadDataTaskAsync(requestMessage.RequestUri, requestMessage.Method, payload).ConfigureAwait(false);
- }
-
- responseHeaders = webClient.ResponseHeaders;
- }
- catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled)
- {
- //TODO: Handle cancellation...
- }
- catch (WebException webException)
- {
- webException.HandleWebException(_serializer);
- }
- finally
- {
- webClient?.Dispose();
- }
-
- return new ApiResponseMessage()
- {
- Headers = responseHeaders,
- Content = response
- };
- }
- #endif
-
-
- private static ApiRequestMessage CreateRequestMessage(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null)
- {
- var req = new ApiRequestMessage()
- {
- RequestUri = address,
- Method = verb,
- };
-
- if (requestOptions != null)
- {
- req.QueryString = requestOptions.QueryString;
- req.ImpersonateUser = requestOptions.ImpersonateUser;
- }
-
- if (content != null)
- {
- req.Content = content;
- }
-
- return req;
- }
-
- private ApiResponseMessage HandleRequest(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null)
- {
- return Send(CreateRequestMessage(address, verb, requestOptions, content));
- }
-
- private ApiResponseMessage Send(ApiRequestMessage requestMessage)
- {
- System.Net.WebClient webClient = null;
- byte[] response = null;
- NameValueCollection responseHeaders = null;
-
- try
- {
- webClient = _webClientFunc();
- SetWebClientHeaders(webClient, requestMessage);
-
- if (IsGetOrDownload(requestMessage.Method))
- {
- response = webClient.DownloadData(requestMessage.RequestUri);
- }
- else
- {
- byte[] payload;
- if (requestMessage.Content != null)
- {
- webClient.Headers.Add(HttpRequestHeader.ContentType, requestMessage.Content.ContentType);
- payload = requestMessage.Content.Body;
- }
- else
- {
- payload = EmptyBytes;
- }
-
- response = webClient.UploadData(requestMessage.RequestUri, requestMessage.Method, payload);
- }
-
- responseHeaders = webClient.ResponseHeaders;
- }
- catch (WebException webException)
- {
- webException.HandleWebException(_serializer);
- }
- finally
- {
- webClient?.Dispose();
- }
-
- return new ApiResponseMessage()
- {
- Headers = responseHeaders,
- Content = response
- };
- }
-
- private void SetWebClientHeaders(System.Net.WebClient webClient, ApiRequestMessage requestMessage)
- {
- if (requestMessage.QueryString != null)
- {
- webClient.QueryString = requestMessage.QueryString;
- }
-
- switch (_credentials)
- {
- case RedmineApiKeyAuthentication:
- webClient.Headers.Add(_credentials.AuthenticationType,_credentials.Token);
- break;
- case RedmineBasicAuthentication:
- webClient.Headers.Add("Authorization", $"{_credentials.AuthenticationType} {_credentials.Token}");
- break;
- }
-
- if (!requestMessage.ImpersonateUser.IsNullOrWhiteSpace())
- {
- webClient.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, requestMessage.ImpersonateUser);
- }
- }
-
- private static bool IsGetOrDownload(string method)
- {
- return method is HttpVerbs.GET or HttpVerbs.DOWNLOAD;
- }
-
- private static string GetContentType(IRedmineSerializer serializer)
- {
- return serializer.Format == RedmineConstants.XML ? RedmineConstants.CONTENT_TYPE_APPLICATION_XML : RedmineConstants.CONTENT_TYPE_APPLICATION_JSON;
- }
- }
-}
diff --git a/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs
deleted file mode 100644
index a1456ad2..00000000
--- a/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-namespace Redmine.Net.Api.Net.WebClient.MessageContent;
-
-internal class ByteArrayApiRequestMessageContent : ApiRequestMessageContent
-{
- public ByteArrayApiRequestMessageContent(byte[] content)
- {
- Body = content;
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs
deleted file mode 100644
index ed49becf..00000000
--- a/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-namespace Redmine.Net.Api.Net.WebClient.MessageContent;
-
-internal sealed class StreamApiRequestMessageContent : ByteArrayApiRequestMessageContent
-{
- public StreamApiRequestMessageContent(byte[] content) : base(content)
- {
- ContentType = RedmineConstants.CONTENT_TYPE_APPLICATION_STREAM;
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs
deleted file mode 100644
index 3a1d7590..00000000
--- a/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Text;
-using Redmine.Net.Api.Internals;
-
-namespace Redmine.Net.Api.Net.WebClient.MessageContent;
-
-internal sealed class StringApiRequestMessageContent : ByteArrayApiRequestMessageContent
-{
- private static readonly Encoding DefaultStringEncoding = Encoding.UTF8;
-
- public StringApiRequestMessageContent(string content, string mediaType) : this(content, mediaType, DefaultStringEncoding)
- {
- }
-
- public StringApiRequestMessageContent(string content, string mediaType, Encoding encoding) : base(GetContentByteArray(content, encoding))
- {
- ContentType = mediaType;
- }
-
- private static byte[] GetContentByteArray(string content, Encoding encoding)
- {
- ArgumentNullThrowHelper.ThrowIfNull(content, nameof(content));
- return (encoding ?? DefaultStringEncoding).GetBytes(content);
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs b/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs
deleted file mode 100644
index 0276931c..00000000
--- a/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs
+++ /dev/null
@@ -1,260 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Net;
-using Redmine.Net.Api.Extensions;
-using Redmine.Net.Api.Serialization;
-
-namespace Redmine.Net.Api
-{
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- #pragma warning disable SYSLIB0014
- public class RedmineWebClient : WebClient
- {
- private string redirectUrl = string.Empty;
-
- ///
- ///
- ///
- public string UserAgent { get; set; }
-
- ///
- /// Gets or sets a value indicating whether [use proxy].
- ///
- ///
- /// true if [use proxy]; otherwise, false.
- ///
- public bool UseProxy { get; set; }
-
- ///
- /// Gets or sets a value indicating whether [use cookies].
- ///
- ///
- /// true if [use cookies]; otherwise, false.
- ///
- public bool UseCookies { get; set; }
-
- ///
- /// in milliseconds
- ///
- ///
- /// The timeout.
- ///
- public TimeSpan? Timeout { get; set; }
-
- ///
- /// Gets or sets the cookie container.
- ///
- ///
- /// The cookie container.
- ///
- public CookieContainer CookieContainer { get; set; }
-
- ///
- /// Gets or sets a value indicating whether [pre authenticate].
- ///
- ///
- /// true if [pre authenticate]; otherwise, false.
- ///
- public bool PreAuthenticate { get; set; }
-
- ///
- /// Gets or sets a value indicating whether [keep alive].
- ///
- ///
- /// true if [keep alive]; otherwise, false.
- ///
- public bool KeepAlive { get; set; }
-
- ///
- ///
- ///
- public string Scheme { get; set; } = "https";
-
- ///
- ///
- ///
- public RedirectType Redirect { get; set; }
-
- ///
- ///
- ///
- internal IRedmineSerializer RedmineSerializer { get; set; }
-
- ///
- /// Returns a object for the specified resource.
- ///
- /// A that identifies the resource to request.
- ///
- /// A new object for the specified resource.
- ///
- protected override WebRequest GetWebRequest(Uri address)
- {
- var wr = base.GetWebRequest(address);
-
- if (!(wr is HttpWebRequest httpWebRequest))
- {
- return base.GetWebRequest(address);
- }
-
- httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate |
- DecompressionMethods.None;
-
- if (UseCookies)
- {
- httpWebRequest.Headers.Add(HttpRequestHeader.Cookie, "redmineCookie");
- httpWebRequest.CookieContainer = CookieContainer;
- }
-
- httpWebRequest.KeepAlive = KeepAlive;
- httpWebRequest.CachePolicy = CachePolicy;
-
- if (Timeout != null)
- {
- httpWebRequest.Timeout = (int)Timeout.Value.TotalMilliseconds;
- }
-
- return httpWebRequest;
- }
-
- ///
- ///
- ///
- ///
- ///
- protected override WebResponse GetWebResponse(WebRequest request)
- {
- WebResponse response = null;
-
- try
- {
- response = base.GetWebResponse(request);
- }
- catch (WebException webException)
- {
- webException.HandleWebException(RedmineSerializer);
- }
-
- switch (response)
- {
- case null:
- return null;
- case HttpWebResponse _:
- HandleRedirect(request, response);
- HandleCookies(request, response);
- break;
- }
-
- return response;
- }
-
- ///
- /// Handles redirect response if needed
- ///
- /// Request
- /// Response
- protected void HandleRedirect(WebRequest request, WebResponse response)
- {
- var webResponse = response as HttpWebResponse;
-
- if (Redirect == RedirectType.None)
- {
- return;
- }
-
- if (webResponse == null)
- {
- return;
- }
-
- var code = webResponse.StatusCode;
-
- if (code == HttpStatusCode.Found || code == HttpStatusCode.SeeOther || code == HttpStatusCode.MovedPermanently || code == HttpStatusCode.Moved)
- {
- redirectUrl = webResponse.Headers["Location"];
-
- var isAbsoluteUri = new Uri(redirectUrl).IsAbsoluteUri;
-
- if (!isAbsoluteUri)
- {
- var webRequest = request as HttpWebRequest;
- var host = webRequest?.Headers["Host"] ?? string.Empty;
-
- if (Redirect == RedirectType.All)
- {
- host = $"{host}{webRequest?.RequestUri.AbsolutePath}";
-
- host = host.Substring(0, host.LastIndexOf('/'));
- }
-
- // Have to make sure that the "/" symbol is between the "host" and "redirect" strings
- #if NET5_0_OR_GREATER
- if (!redirectUrl.StartsWith('/') && !host.EndsWith('/'))
- #else
- if (!redirectUrl.StartsWith("/", StringComparison.OrdinalIgnoreCase) && !host.EndsWith("/", StringComparison.OrdinalIgnoreCase))
- #endif
- {
- redirectUrl = $"/{redirectUrl}";
- }
-
- redirectUrl = $"{host}{redirectUrl}";
- }
-
- if (!redirectUrl.StartsWith(Scheme, StringComparison.OrdinalIgnoreCase))
- {
- redirectUrl = $"{Scheme}://{redirectUrl}";
- }
- }
- else
- {
- redirectUrl = string.Empty;
- }
- }
-
- ///
- /// Handles additional cookies
- ///
- /// Request
- /// Response
- protected void HandleCookies(WebRequest request, WebResponse response)
- {
- if (!(response is HttpWebResponse webResponse)) return;
-
- var webRequest = request as HttpWebRequest;
-
- if (webResponse.Cookies.Count <= 0) return;
-
- var col = new CookieCollection();
-
- foreach (Cookie c in webResponse.Cookies)
- {
- col.Add(new Cookie(c.Name, c.Value, c.Path, webRequest?.Headers["Host"]));
- }
-
- if (CookieContainer == null)
- {
- CookieContainer = new CookieContainer();
- }
-
- CookieContainer.Add(col);
- }
- }
- #pragma warning restore SYSLIB0014
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/RedmineManagerOptions.cs b/src/redmine-net-api/Options/RedmineManagerOptions.cs
similarity index 65%
rename from src/redmine-net-api/RedmineManagerOptions.cs
rename to src/redmine-net-api/Options/RedmineManagerOptions.cs
index 3801922b..7093c073 100644
--- a/src/redmine-net-api/RedmineManagerOptions.cs
+++ b/src/redmine-net-api/Options/RedmineManagerOptions.cs
@@ -17,11 +17,16 @@ limitations under the License.
using System;
using System.Net;
using Redmine.Net.Api.Authentication;
-using Redmine.Net.Api.Net;
-using Redmine.Net.Api.Net.WebClient;
+using Redmine.Net.Api.Http;
+using Redmine.Net.Api.Http.Clients.WebClient;
+using Redmine.Net.Api.Logging;
using Redmine.Net.Api.Serialization;
+#if !NET20
+using System.Net.Http;
+using Redmine.Net.Api.Http.Clients.HttpClient;
+#endif
-namespace Redmine.Net.Api
+namespace Redmine.Net.Api.Options
{
///
///
@@ -53,6 +58,23 @@ internal sealed class RedmineManagerOptions
///
public IRedmineAuthentication Authentication { get; init; }
+ ///
+ /// Gets or sets the version of the Redmine server to which this client will connect.
+ ///
+ public Version RedmineVersion { get; init; }
+
+ public IRedmineLogger Logger { get; init; }
+
+ ///
+ /// Gets or sets additional logging configuration options
+ ///
+ public RedmineLoggingOptions LoggingOptions { get; init; } = new RedmineLoggingOptions();
+
+ ///
+ /// Gets or sets the settings for configuring the Redmine http client.
+ ///
+ public IRedmineApiClientOptions ApiClientOptions { get; set; }
+
///
/// Gets or sets a custom function that creates and returns a specialized instance of the WebClient class.
///
@@ -61,13 +83,24 @@ internal sealed class RedmineManagerOptions
///
/// Gets or sets the settings for configuring the Redmine web client.
///
- public IRedmineWebClientOptions WebClientOptions { get; init; }
+ public RedmineWebClientOptions WebClientOptions {
+ get => (RedmineWebClientOptions)ApiClientOptions;
+ set => ApiClientOptions = value;
+ }
+ #if !NET20
///
- /// Gets or sets the version of the Redmine server to which this client will connect.
+ ///
///
- public Version RedmineVersion { get; init; }
+ public HttpClient HttpClient { get; init; }
- internal bool VerifyServerCert { get; init; }
+ ///
+ /// Gets or sets the settings for configuring the Redmine http client.
+ ///
+ public RedmineHttpClientOptions HttpClientOptions {
+ get => (RedmineHttpClientOptions)ApiClientOptions;
+ set => ApiClientOptions = value;
+ }
+ #endif
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Options/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/Options/RedmineManagerOptionsBuilder.cs
new file mode 100644
index 00000000..698cfb31
--- /dev/null
+++ b/src/redmine-net-api/Options/RedmineManagerOptionsBuilder.cs
@@ -0,0 +1,306 @@
+/*
+ Copyright 2011 - 2025 Adrian Popescu
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+using System;
+using System.Net;
+#if NET462_OR_GREATER || NET
+using Microsoft.Extensions.Logging;
+#endif
+using Redmine.Net.Api.Authentication;
+using Redmine.Net.Api.Http;
+#if !NET20
+using Redmine.Net.Api.Http.Clients.HttpClient;
+#endif
+using Redmine.Net.Api.Http.Clients.WebClient;
+using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Logging;
+using Redmine.Net.Api.Serialization;
+#if NET40_OR_GREATER || NET
+using System.Net.Http;
+#endif
+#if NET462_OR_GREATER || NET
+#endif
+
+namespace Redmine.Net.Api.Options
+{
+ ///
+ ///
+ ///
+ public sealed class RedmineManagerOptionsBuilder
+ {
+ private IRedmineLogger _redmineLogger = RedmineNullLogger.Instance;
+ private Action _configureLoggingOptions;
+
+ private enum ClientType
+ {
+ WebClient,
+ HttpClient,
+ }
+ private ClientType _clientType = ClientType.HttpClient;
+
+ ///
+ ///
+ ///
+ public string Host { get; private set; }
+
+ ///
+ ///
+ ///
+ public int PageSize { get; private set; }
+
+ ///
+ /// Gets the current serialization type
+ ///
+ public SerializationType SerializationType { get; private set; }
+
+ ///
+ ///
+ ///
+ public IRedmineAuthentication Authentication { get; private set; }
+
+ ///
+ ///
+ ///
+ public IRedmineApiClientOptions ClientOptions { get; private set; }
+
+ ///
+ ///
+ ///
+ public Func ClientFunc { get; private set; }
+
+ ///
+ /// Gets or sets the version of the Redmine server to which this client will connect.
+ ///
+ public Version Version { get; set; }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedmineManagerOptionsBuilder WithPageSize(int pageSize)
+ {
+ PageSize = pageSize;
+ return this;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedmineManagerOptionsBuilder WithHost(string baseAddress)
+ {
+ Host = baseAddress;
+ return this;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedmineManagerOptionsBuilder WithSerializationType(SerializationType serializationType)
+ {
+ SerializationType = serializationType;
+ return this;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public RedmineManagerOptionsBuilder WithXmlSerialization()
+ {
+ SerializationType = SerializationType.Xml;
+ return this;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public RedmineManagerOptionsBuilder WithJsonSerialization()
+ {
+ SerializationType = SerializationType.Json;
+ return this;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedmineManagerOptionsBuilder WithApiKeyAuthentication(string apiKey)
+ {
+ Authentication = new RedmineApiKeyAuthentication(apiKey);
+ return this;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedmineManagerOptionsBuilder WithBasicAuthentication(string login, string password)
+ {
+ Authentication = new RedmineBasicAuthentication(login, password);
+ return this;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedmineManagerOptionsBuilder WithLogger(IRedmineLogger logger, Action configure = null)
+ {
+ _redmineLogger = logger ?? RedmineNullLogger.Instance;
+ _configureLoggingOptions = configure;
+ return this;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedmineManagerOptionsBuilder WithVersion(Version version)
+ {
+ Version = version;
+ return this;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedmineManagerOptionsBuilder WithWebClient(Func clientFunc)
+ {
+ _clientType = ClientType.WebClient;
+ ClientFunc = clientFunc;
+ return this;
+ }
+
+ ///
+ /// Configures the client to use WebClient with default settings
+ ///
+ /// This builder instance for method chaining
+ public RedmineManagerOptionsBuilder UseWebClient(RedmineWebClientOptions clientOptions = null)
+ {
+ _clientType = ClientType.WebClient;
+ ClientOptions = clientOptions;
+ return this;
+ }
+
+#if NET40_OR_GREATER || NET
+ ///
+ ///
+ ///
+ public Func HttpClientFunc { get; private set; }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedmineManagerOptionsBuilder WithHttpClient(Func clientFunc)
+ {
+ _clientType = ClientType.HttpClient;
+ this.HttpClientFunc = clientFunc;
+ return this;
+ }
+
+ ///
+ /// Configures the client to use HttpClient with default settings
+ ///
+ /// This builder instance for method chaining
+ public RedmineManagerOptionsBuilder UseHttpClient(RedmineHttpClientOptions clientOptions = null)
+ {
+ _clientType = ClientType.HttpClient;
+ ClientOptions = clientOptions;
+ return this;
+ }
+
+#endif
+
+#if NET462_OR_GREATER || NET
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedmineManagerOptionsBuilder WithLogger(ILogger logger, Action configure = null)
+ {
+ _redmineLogger = new MicrosoftLoggerRedmineAdapter(logger);
+ _configureLoggingOptions = configure;
+ return this;
+ }
+#endif
+
+ ///
+ ///
+ ///
+ ///
+ internal RedmineManagerOptions Build()
+ {
+#if NET45_OR_GREATER || NET
+ ClientOptions ??= _clientType switch
+ {
+ ClientType.WebClient => new RedmineWebClientOptions(),
+ ClientType.HttpClient => new RedmineHttpClientOptions(),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+#else
+ ClientOptions ??= new RedmineWebClientOptions();
+#endif
+
+ var baseAddress = HostHelper.CreateRedmineUri(Host, ClientOptions.Scheme);
+
+ var redmineLoggingOptions = ConfigureLoggingOptions();
+
+ var options = new RedmineManagerOptions()
+ {
+ BaseAddress = baseAddress,
+ PageSize = PageSize > 0 ? PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE,
+ Serializer = RedmineSerializerFactory.CreateSerializer(SerializationType),
+ RedmineVersion = Version,
+ Authentication = Authentication ?? new RedmineNoAuthentication(),
+ ApiClientOptions = ClientOptions,
+ Logger = _redmineLogger,
+ LoggingOptions = redmineLoggingOptions,
+ };
+
+ return options;
+ }
+
+ private RedmineLoggingOptions ConfigureLoggingOptions()
+ {
+ if (_configureLoggingOptions == null)
+ {
+ return null;
+ }
+
+ var redmineLoggingOptions = new RedmineLoggingOptions();
+ _configureLoggingOptions(redmineLoggingOptions);
+ return redmineLoggingOptions;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/RedmineConstants.cs b/src/redmine-net-api/RedmineConstants.cs
index e9d00588..fa752ce4 100644
--- a/src/redmine-net-api/RedmineConstants.cs
+++ b/src/redmine-net-api/RedmineConstants.cs
@@ -48,9 +48,26 @@ public static class RedmineConstants
///
public const string IMPERSONATE_HEADER_KEY = "X-Redmine-Switch-User";
+ ///
+ ///
+ ///
+ public const string AUTHORIZATION_HEADER_KEY = "Authorization";
+ ///
+ ///
+ ///
+ public const string API_KEY_AUTHORIZATION_HEADER_KEY = "X-Redmine-API-Key";
+
///
///
///
public const string XML = "xml";
+
+ ///
+ ///
+ ///
+ public const string JSON = "json";
+
+ internal const string USER_AGENT_HEADER_KEY = "User-Agent";
+ internal const string CONTENT_TYPE_HEADER_KEY = "Content-Type";
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs
index 045533cb..b5072aa6 100644
--- a/src/redmine-net-api/RedmineKeys.cs
+++ b/src/redmine-net-api/RedmineKeys.cs
@@ -179,8 +179,13 @@ public static class RedmineKeys
///
///
///
- public const string CUSTOM_FIELDS = "custom_fields";
+ public const string CUSTOM_FIELD_VALUES = "custom_field_values";
+ ///
+ ///
+ ///
+ public const string CUSTOM_FIELDS = "custom_fields";
+
///
///
///
@@ -269,6 +274,10 @@ public static class RedmineKeys
///
///
///
+ public const string ENUMERATION_DOCUMENT_CATEGORIES = "enumerations/document_categories";
+ ///
+ ///
+ ///
public const string ENUMERATION_ISSUE_PRIORITIES = "enumerations/issue_priorities";
///
///
@@ -326,6 +335,10 @@ public static class RedmineKeys
///
///
///
+ public const string GENERATE_PASSWORD = "generate_password";
+ ///
+ ///
+ ///
public const string GROUP = "group";
///
///
@@ -383,7 +396,10 @@ public static class RedmineKeys
///
///
public const string ISSUE_CATEGORY = "issue_category";
-
+ ///
+ ///
+ ///
+ public const string ISSUE_CUSTOM_FIELDS = "issue_custom_fields";
///
///
///
@@ -684,6 +700,10 @@ public static class RedmineKeys
///
///
///
+ public const string SEND_INFORMATION = "send_information";
+ ///
+ ///
+ ///
public const string SEARCH = "search";
///
///
diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManager.Async.cs
similarity index 95%
rename from src/redmine-net-api/RedmineManagerAsync.cs
rename to src/redmine-net-api/RedmineManager.Async.cs
index 72e2d6a6..bf48328e 100644
--- a/src/redmine-net-api/RedmineManagerAsync.cs
+++ b/src/redmine-net-api/RedmineManager.Async.cs
@@ -20,11 +20,17 @@ limitations under the License.
using System.Collections.Specialized;
using System.Threading;
using System.Threading.Tasks;
+using Redmine.Net.Api.Common;
using Redmine.Net.Api.Extensions;
+using Redmine.Net.Api.Http;
+using Redmine.Net.Api.Http.Extensions;
using Redmine.Net.Api.Net;
+using Redmine.Net.Api.Net.Internal;
using Redmine.Net.Api.Serialization;
using Redmine.Net.Api.Types;
+#if!(NET45_OR_GREATER || NETCOREAPP)
using TaskExtensions = Redmine.Net.Api.Extensions.TaskExtensions;
+#endif
namespace Redmine.Net.Api;
@@ -211,15 +217,15 @@ public async Task UploadFileAsync(byte[] data, string fileName = null, R
{
var url = RedmineApiUrls.UploadFragment(fileName);
- var response = await ApiClient.UploadFileAsync(url, data,requestOptions , cancellationToken: cancellationToken).ConfigureAwait(false);
+ var response = await ApiClient.UploadFileAsync(url, data, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
return response.DeserializeTo(Serializer);
}
///
- public async Task DownloadFileAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
+ public async Task DownloadFileAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default)
{
- var response = await ApiClient.DownloadAsync(address, requestOptions,cancellationToken: cancellationToken).ConfigureAwait(false);
+ var response = await ApiClient.DownloadAsync(address, requestOptions, progress, cancellationToken: cancellationToken).ConfigureAwait(false);
return response.Content;
}
diff --git a/src/redmine-net-api/RedmineManager.Obsolete.cs b/src/redmine-net-api/RedmineManager.Obsolete.cs
deleted file mode 100644
index 950ecafb..00000000
--- a/src/redmine-net-api/RedmineManager.Obsolete.cs
+++ /dev/null
@@ -1,617 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-using System.Net;
-using System.Net.Security;
-using System.Security.Cryptography.X509Certificates;
-using Redmine.Net.Api.Exceptions;
-using Redmine.Net.Api.Extensions;
-using Redmine.Net.Api.Net;
-using Redmine.Net.Api.Net.WebClient;
-using Redmine.Net.Api.Serialization;
-using Redmine.Net.Api.Types;
-
-namespace Redmine.Net.Api
-{
- ///
- /// The main class to access Redmine API.
- ///
- public partial class RedmineManager
- {
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineConstants.DEFAULT_PAGE_SIZE")]
- public const int DEFAULT_PAGE_SIZE_VALUE = 25;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The host.
- /// The MIME format.
- /// if set to true [verify server cert].
- /// The proxy.
- /// Use this parameter to specify a SecurityProtocolType.
- /// Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process.
- /// http or https. Default is https.
- /// The webclient timeout. Default is 100 seconds.
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManager(RedmineManagerOptionsBuilder")]
- public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true,
- IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null)
- :this(new RedmineManagerOptionsBuilder()
- .WithHost(host)
- .WithSerializationType(mimeFormat)
- .WithVerifyServerCert(verifyServerCert)
- .WithWebClientOptions(new RedmineWebClientOptions()
- {
- Proxy = proxy,
- Scheme = scheme,
- Timeout = timeout,
- SecurityProtocolType = securityProtocolType
- })
- ) { }
-
- ///
- /// Initializes a new instance of the class using your API key for authentication.
- ///
- ///
- /// To enable the API-style authentication, you have to check Enable REST API in Administration -> Settings -> Authentication.
- /// You can find your API key on your account page ( /my/account ) when logged in, on the right-hand pane of the default layout.
- ///
- /// The host.
- /// The API key.
- /// The MIME format.
- /// if set to true [verify server cert].
- /// The proxy.
- /// Use this parameter to specify a SecurityProtocolType.
- /// Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process.
- ///
- /// The webclient timeout. Default is 100 seconds.
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManager(RedmineManagerOptionsBuilder")]
- public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFormat.Xml,
- bool verifyServerCert = true, IWebProxy proxy = null,
- SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null)
- : this(new RedmineManagerOptionsBuilder()
- .WithHost(host)
- .WithApiKeyAuthentication(apiKey)
- .WithSerializationType(mimeFormat)
- .WithVerifyServerCert(verifyServerCert)
- .WithWebClientOptions(new RedmineWebClientOptions()
- {
- Proxy = proxy,
- Scheme = scheme,
- Timeout = timeout,
- SecurityProtocolType = securityProtocolType
- })){}
-
- ///
- /// Initializes a new instance of the class using your login and password for authentication.
- ///
- /// The host.
- /// The login.
- /// The password.
- /// The MIME format.
- /// if set to true [verify server cert].
- /// The proxy.
- /// Use this parameter to specify a SecurityProtocolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process.
- ///
- /// The webclient timeout. Default is 100 seconds.
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManager(RedmineManagerOptionsBuilder")]
- public RedmineManager(string host, string login, string password, MimeFormat mimeFormat = MimeFormat.Xml,
- bool verifyServerCert = true, IWebProxy proxy = null,
- SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null)
- : this(new RedmineManagerOptionsBuilder()
- .WithHost(host)
- .WithBasicAuthentication(login, password)
- .WithSerializationType(mimeFormat)
- .WithVerifyServerCert(verifyServerCert)
- .WithWebClientOptions(new RedmineWebClientOptions()
- {
- Proxy = proxy,
- Scheme = scheme,
- Timeout = timeout,
- SecurityProtocolType = securityProtocolType
- })) {}
-
-
- ///
- /// Gets the suffixes.
- ///
- ///
- /// The suffixes.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + " It returns null.")]
- public static Dictionary Suffixes => null;
-
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public string Format { get; }
-
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public string Scheme { get; }
-
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public TimeSpan? Timeout { get; }
-
- ///
- /// Gets the host.
- ///
- ///
- /// The host.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public string Host { get; }
-
- ///
- /// The ApiKey used to authenticate.
- ///
- ///
- /// The API key.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public string ApiKey { get; }
-
- ///
- /// Gets the MIME format.
- ///
- ///
- /// The MIME format.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public MimeFormat MimeFormat { get; }
-
- ///
- /// Gets the proxy.
- ///
- ///
- /// The proxy.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public IWebProxy Proxy { get; }
-
- ///
- /// Gets the type of the security protocol.
- ///
- ///
- /// The type of the security protocol.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public SecurityProtocolType SecurityProtocolType { get; }
-
-
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public int PageSize { get; set; }
-
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public string ImpersonateUser { get; set; }
-
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT )]
- public static readonly Dictionary TypesWithOffset = new Dictionary{
- {typeof(Issue), true},
- {typeof(Project), true},
- {typeof(User), true},
- {typeof(News), true},
- {typeof(Query), true},
- {typeof(TimeEntry), true},
- {typeof(ProjectMembership), true},
- {typeof(Search), true}
- };
-
- ///
- /// Returns the user whose credentials are used to access the API.
- ///
- /// The accepted parameters are: memberships and groups (added in 2.1).
- ///
- ///
- /// An error occurred during deserialization. The original exception is available
- /// using the System.Exception.InnerException property.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use GetCurrentUser extension instead")]
- public User GetCurrentUser(NameValueCollection parameters = null)
- {
- return this.GetCurrentUser(RedmineManagerExtensions.CreateRequestOptions(parameters));
- }
-
- ///
- ///
- ///
- /// Returns the my account details.
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use GetMyAccount extension instead")]
- public MyAccount GetMyAccount()
- {
- return RedmineManagerExtensions.GetMyAccount(this);
- }
-
- ///
- /// Adds the watcher to issue.
- ///
- /// The issue identifier.
- /// The user identifier.
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use AddWatcherToIssue extension instead")]
- public void AddWatcherToIssue(int issueId, int userId)
- {
- RedmineManagerExtensions.AddWatcherToIssue(this, issueId, userId);
- }
-
- ///
- /// Removes the watcher from issue.
- ///
- /// The issue identifier.
- /// The user identifier.
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use RemoveWatcherFromIssue extension instead")]
- public void RemoveWatcherFromIssue(int issueId, int userId)
- {
- RedmineManagerExtensions.RemoveWatcherFromIssue(this, issueId, userId);
- }
-
- ///
- /// Adds an existing user to a group.
- ///
- /// The group id.
- /// The user id.
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use AddUserToGroup extension instead")]
- public void AddUserToGroup(int groupId, int userId)
- {
- RedmineManagerExtensions.AddUserToGroup(this, groupId, userId);
- }
-
- ///
- /// Removes an user from a group.
- ///
- /// The group id.
- /// The user id.
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use RemoveUserFromGroup extension instead")]
- public void RemoveUserFromGroup(int groupId, int userId)
- {
- RedmineManagerExtensions.RemoveUserFromGroup(this, groupId, userId);
- }
-
- ///
- /// Creates or updates a wiki page.
- ///
- /// The project id or identifier.
- /// The wiki page name.
- /// The wiki page to create or update.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use UpdateWikiPage extension instead")]
- public void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage)
- {
- RedmineManagerExtensions.UpdateWikiPage(this, projectId, pageName, wikiPage);
- }
-
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use CreateWikiPage extension instead")]
- public WikiPage CreateWikiPage(string projectId, string pageName, WikiPage wikiPage)
- {
- return RedmineManagerExtensions.CreateWikiPage(this, projectId, pageName, wikiPage);
- }
-
- ///
- /// Gets the wiki page.
- ///
- /// The project identifier.
- /// The parameters.
- /// Name of the page.
- /// The version.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use GetWikiPage extension instead")]
- public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0)
- {
- return this.GetWikiPage(projectId, pageName, RedmineManagerExtensions.CreateRequestOptions(parameters), version);
- }
-
- ///
- /// Returns the list of all pages in a project wiki.
- ///
- /// The project id or identifier.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use GetAllWikiPages extension instead")]
- public List GetAllWikiPages(string projectId)
- {
- return RedmineManagerExtensions.GetAllWikiPages(this, projectId);
- }
-
- ///
- /// Deletes a wiki page, its attachments and its history. If the deleted page is a parent page, its child pages are not
- /// deleted but changed as root pages.
- ///
- /// The project id or identifier.
- /// The wiki page name.
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use DeleteWikiPage extension instead")]
- public void DeleteWikiPage(string projectId, string pageName)
- {
- RedmineManagerExtensions.DeleteWikiPage(this, projectId, pageName);
- }
-
- ///
- /// Updates the attachment.
- ///
- /// The issue identifier.
- /// The attachment.
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use UpdateAttachment extension instead")]
- public void UpdateAttachment(int issueId, Attachment attachment)
- {
- this.UpdateIssueAttachment(issueId, attachment);
- }
-
- ///
- ///
- ///
- /// query strings. enable to specify multiple values separated by a space " ".
- /// number of results in response.
- /// skip this number of results in response
- /// Optional filters.
- ///
- /// Returns the search results by the specified condition parameters.
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use Search extension instead")]
- public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null)
- {
- return RedmineManagerExtensions.Search(this, q, limit, offset, searchFilter);
- }
-
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public int Count(params string[] include) where T : class, new()
- {
- var parameters = NameValueCollectionExtensions.AddParamsIfExist(null, include);
-
- return Count(parameters);
- }
-
- ///
- ///
- ///
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public int Count(NameValueCollection parameters) where T : class, new()
- {
- return Count(parameters != null ? new RequestOptions { QueryString = parameters } : null);
- }
-
- ///
- /// Gets the redmine object based on id.
- ///
- /// The type of objects to retrieve.
- /// The id of the object.
- /// Optional filters and/or optional fetched data.
- ///
- /// Returns the object of type T.
- ///
- ///
- ///
- /// string issueId = "927";
- /// NameValueCollection parameters = null;
- /// Issue issue = redmineManager.GetObject<Issue>(issueId, parameters);
- ///
- ///
- [Obsolete($"{RedmineConstants.OBSOLETE_TEXT} Use Get instead")]
- public T GetObject(string id, NameValueCollection parameters) where T : class, new()
- {
- return Get(id, parameters != null ? new RequestOptions { QueryString = parameters } : null);
- }
-
- ///
- /// Returns the complete list of objects.
- ///
- ///
- /// Optional fetched data.
- ///
- /// Optional fetched data:
- /// Project: trackers, issue_categories, enabled_modules (since Redmine 2.6.0)
- /// Issue: children, attachments, relations, changesets, journals, watchers (since Redmine 2.3.0)
- /// Users: memberships, groups (since Redmine 2.1)
- /// Groups: users, memberships
- ///
- /// Returns the complete list of objects.
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public List GetObjects(params string[] include) where T : class, new()
- {
- var parameters = NameValueCollectionExtensions.AddParamsIfExist(null, include);
-
- return GetObjects(parameters);
- }
-
- ///
- /// Returns the complete list of objects.
- ///
- ///
- /// The page size.
- /// The offset.
- /// Optional fetched data.
- ///
- /// Optional fetched data:
- /// Project: trackers, issue_categories, enabled_modules (since 2.6.0)
- /// Issue: children, attachments, relations, changesets, journals, watchers - Since 2.3.0
- /// Users: memberships, groups (added in 2.1)
- /// Groups: users, memberships
- ///
- /// Returns the complete list of objects.
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public List GetObjects(int limit, int offset, params string[] include) where T : class, new()
- {
- var parameters = NameValueCollectionExtensions
- .AddParamsIfExist(null, include)
- .AddPagingParameters(limit, offset);
-
- return GetObjects(parameters);
- }
-
- ///
- /// Returns the complete list of objects.
- ///
- /// The type of objects to retrieve.
- /// Optional filters and/or optional fetched data.
- ///
- /// Returns a complete list of objects.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public List GetObjects(NameValueCollection parameters = null) where T : class, new()
- {
- return Get(parameters != null ? new RequestOptions { QueryString = parameters } : null);
- }
-
- ///
- /// Gets the paginated objects.
- ///
- ///
- /// The parameters.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new()
- {
- return GetPaginated(parameters != null ? new RequestOptions { QueryString = parameters } : null);
- }
-
- ///
- /// Creates a new Redmine object.
- ///
- /// The type of object to create.
- /// The object to create.
- ///
- ///
- ///
- /// When trying to create an object with invalid or missing attribute parameters, you will get a 422 Unprocessable
- /// Entity response. That means that the object could not be created.
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public T CreateObject(T entity) where T : class, new()
- {
- return Create(entity);
- }
-
- ///
- /// Creates a new Redmine object.
- ///
- /// The type of object to create.
- /// The object to create.
- /// The owner identifier.
- ///
- ///
- ///
- /// When trying to create an object with invalid or missing attribute parameters, you will get a 422 Unprocessable
- /// Entity response. That means that the object could not be created.
- ///
- ///
- ///
- /// var project = new Project();
- /// project.Name = "test";
- /// project.Identifier = "the project identifier";
- /// project.Description = "the project description";
- /// redmineManager.CreateObject(project);
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public T CreateObject(T entity, string ownerId) where T : class, new()
- {
- return Create(entity, ownerId);
- }
-
- ///
- /// Updates a Redmine object.
- ///
- /// The type of object to be updated.
- /// The id of the object to be updated.
- /// The object to be updated.
- /// The project identifier.
- ///
- ///
- /// When trying to update an object with invalid or missing attribute parameters, you will get a
- /// 422(RedmineException) Unprocessable Entity response. That means that the object could not be updated.
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public void UpdateObject(string id, T entity, string projectId = null) where T : class, new()
- {
- Update(id, entity, projectId);
- }
-
- ///
- /// Deletes the Redmine object.
- ///
- /// The type of objects to delete.
- /// The id of the object to delete
- /// The parameters
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT)]
- public void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new()
- {
- Delete(id, parameters != null ? new RequestOptions { QueryString = parameters } : null);
- }
-
- ///
- /// Creates the Redmine web client.
- ///
- /// The parameters.
- /// if set to true [upload file].
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "If a custom webClient is needed, use Func from RedmineManagerSettings instead")]
- public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false)
- {
- throw new NotImplementedException();
- }
-
- ///
- /// This is to take care of SSL certification validation which are not issued by Trusted Root CA.
- ///
- /// The sender.
- /// The cert.
- /// The chain.
- /// The error.
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use RedmineManagerOptions.ClientOptions.ServerCertificateValidationCallback instead")]
- public virtual bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors)
- {
- const SslPolicyErrors IGNORED_ERRORS = SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch;
-
- return (sslPolicyErrors & ~IGNORED_ERRORS) == SslPolicyErrors.None;
-
- }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs
index 6e10e7b4..ca062ef8 100644
--- a/src/redmine-net-api/RedmineManager.cs
+++ b/src/redmine-net-api/RedmineManager.cs
@@ -17,13 +17,19 @@ limitations under the License.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
-using System.Globalization;
using System.Net;
-using Redmine.Net.Api.Authentication;
+using Redmine.Net.Api.Common;
using Redmine.Net.Api.Extensions;
+using Redmine.Net.Api.Http;
+using Redmine.Net.Api.Http.Clients.WebClient;
+using Redmine.Net.Api.Http.Extensions;
using Redmine.Net.Api.Internals;
-using Redmine.Net.Api.Net;
-using Redmine.Net.Api.Net.WebClient;
+using Redmine.Net.Api.Logging;
+#if NET40_OR_GREATER || NET
+using Redmine.Net.Api.Http.Clients.HttpClient;
+#endif
+using Redmine.Net.Api.Net.Internal;
+using Redmine.Net.Api.Options;
using Redmine.Net.Api.Serialization;
using Redmine.Net.Api.Types;
@@ -39,6 +45,7 @@ public partial class RedmineManager : IRedmineManager
internal IRedmineSerializer Serializer { get; }
internal RedmineApiUrls RedmineApiUrls { get; }
internal IRedmineApiClient ApiClient { get; }
+ internal IRedmineLogger Logger { get; }
///
///
@@ -50,56 +57,97 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder)
ArgumentNullThrowHelper.ThrowIfNull(optionsBuilder, nameof(optionsBuilder));
_redmineManagerOptions = optionsBuilder.Build();
- #if NET45_OR_GREATER
- if (_redmineManagerOptions.VerifyServerCert)
+
+ Logger = _redmineManagerOptions.Logger;
+ Serializer = _redmineManagerOptions.Serializer;
+ RedmineApiUrls = new RedmineApiUrls(_redmineManagerOptions.Serializer.Format);
+
+ ApiClient =
+#if NET40_OR_GREATER || NET
+ _redmineManagerOptions.ApiClientOptions switch
+ {
+ RedmineWebClientOptions => CreateWebClient(_redmineManagerOptions),
+ RedmineHttpClientOptions => CreateHttpClient(_redmineManagerOptions),
+ };
+#else
+ CreateWebClient(_redmineManagerOptions);
+#endif
+ }
+
+ private static InternalRedmineApiWebClient CreateWebClient(RedmineManagerOptions options)
+ {
+ if (options.ClientFunc != null)
{
- _redmineManagerOptions.WebClientOptions.ServerCertificateValidationCallback = RemoteCertValidate;
+ return new InternalRedmineApiWebClient(options.ClientFunc, options);
}
- #endif
- if (_redmineManagerOptions.WebClientOptions is RedmineWebClientOptions)
+ ApplyServiceManagerSettings(options.WebClientOptions);
+#pragma warning disable SYSLIB0014
+ options.WebClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol;
+#pragma warning restore SYSLIB0014
+
+ return new InternalRedmineApiWebClient(options);
+ }
+#if NET40_OR_GREATER || NET
+ private InternalRedmineApiHttpClient CreateHttpClient(RedmineManagerOptions options)
+ {
+ return options.HttpClient != null
+ ? new InternalRedmineApiHttpClient(options.HttpClient, options)
+ : new InternalRedmineApiHttpClient(_redmineManagerOptions);
+ }
+#endif
+
+ private static void ApplyServiceManagerSettings(RedmineWebClientOptions options)
+ {
+ if (options == null)
{
- Proxy = _redmineManagerOptions.WebClientOptions.Proxy;
- Timeout = _redmineManagerOptions.WebClientOptions.Timeout;
- SecurityProtocolType = _redmineManagerOptions.WebClientOptions.SecurityProtocolType.GetValueOrDefault();
- #pragma warning disable SYSLIB0014
- _redmineManagerOptions.WebClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol;
- #pragma warning restore SYSLIB0014
+ return;
}
-
- if (_redmineManagerOptions.Authentication is RedmineApiKeyAuthentication)
+
+ if (options.SecurityProtocolType.HasValue)
{
- ApiKey = _redmineManagerOptions.Authentication.Token;
+ ServicePointManager.SecurityProtocol = options.SecurityProtocolType.Value;
}
-
- Serializer = _redmineManagerOptions.Serializer;
- Host = _redmineManagerOptions.BaseAddress.ToString();
- PageSize = _redmineManagerOptions.PageSize;
- Scheme = _redmineManagerOptions.BaseAddress.Scheme;
- Format = Serializer.Format;
- MimeFormat = RedmineConstants.XML.Equals(Serializer.Format, StringComparison.Ordinal)
- ? MimeFormat.Xml
- : MimeFormat.Json;
-
- RedmineApiUrls = new RedmineApiUrls(Serializer.Format);
- #if NET45_OR_GREATER || NETCOREAPP
- if (_redmineManagerOptions.WebClientOptions is RedmineWebClientOptions)
+
+ if (options.DefaultConnectionLimit.HasValue)
{
- ApiClient = _redmineManagerOptions.ClientFunc != null
- ? new InternalRedmineApiWebClient(_redmineManagerOptions.ClientFunc, _redmineManagerOptions.Authentication, _redmineManagerOptions.Serializer)
- : new InternalRedmineApiWebClient(_redmineManagerOptions);
+ ServicePointManager.DefaultConnectionLimit = options.DefaultConnectionLimit.Value;
}
- else
+
+ if (options.DnsRefreshTimeout.HasValue)
{
-
+ ServicePointManager.DnsRefreshTimeout = options.DnsRefreshTimeout.Value;
}
- #else
- ApiClient = _redmineManagerOptions.ClientFunc != null
- ? new InternalRedmineApiWebClient(_redmineManagerOptions.ClientFunc, _redmineManagerOptions.Authentication, _redmineManagerOptions.Serializer)
- : new InternalRedmineApiWebClient(_redmineManagerOptions);
- #endif
+
+ if (options.EnableDnsRoundRobin.HasValue)
+ {
+ ServicePointManager.EnableDnsRoundRobin = options.EnableDnsRoundRobin.Value;
+ }
+
+ if (options.MaxServicePoints.HasValue)
+ {
+ ServicePointManager.MaxServicePoints = options.MaxServicePoints.Value;
+ }
+
+ if (options.MaxServicePointIdleTime.HasValue)
+ {
+ ServicePointManager.MaxServicePointIdleTime = options.MaxServicePointIdleTime.Value;
+ }
+
+#if(NET46_OR_GREATER || NET)
+ if (options.ReusePort.HasValue)
+ {
+ ServicePointManager.ReusePort = options.ReusePort.Value;
+ }
+#endif
+ #if NEFRAMEWORK
+ if (options.CheckCertificateRevocationList)
+ {
+ ServicePointManager.CheckCertificateRevocationList = true;
+ }
+#endif
}
-
+
///
public int Count(RequestOptions requestOptions = null)
where T : class, new()
@@ -108,10 +156,7 @@ public int Count(RequestOptions requestOptions = null)
const int PAGE_SIZE = 1;
const int OFFSET = 0;
- if (requestOptions == null)
- {
- requestOptions = new RequestOptions();
- }
+ requestOptions ??= new RequestOptions();
requestOptions.QueryString = requestOptions.QueryString.AddPagingParameters(PAGE_SIZE, OFFSET);
@@ -200,9 +245,9 @@ public Upload UploadFile(byte[] data, string fileName = null)
}
///
- public byte[] DownloadFile(string address)
+ public byte[] DownloadFile(string address, IProgress progress = null)
{
- var response = ApiClient.Download(address);
+ var response = ApiClient.Download(address, progress: progress);
return response.Content;
}
@@ -236,7 +281,7 @@ internal List GetInternal(string uri, RequestOptions requestOptions = null
if (pageSize == default)
{
pageSize = _redmineManagerOptions.PageSize > 0 ? _redmineManagerOptions.PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE;
- requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture));
+ requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString());
}
var hasOffset = TypesWithOffset.ContainsKey(typeof(T));
@@ -245,7 +290,7 @@ internal List GetInternal(string uri, RequestOptions requestOptions = null
int totalCount;
do
{
- requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture));
+ requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToInvariantString());
var tempResult = GetPaginatedInternal(uri, requestOptions);
@@ -295,5 +340,16 @@ internal PagedResults GetPaginatedInternal(string uri = null, RequestOptio
return response.DeserializeToPagedResults(Serializer);
}
+
+ internal static readonly Dictionary TypesWithOffset = new Dictionary{
+ {typeof(Issue), true},
+ {typeof(Project), true},
+ {typeof(User), true},
+ {typeof(News), true},
+ {typeof(Query), true},
+ {typeof(TimeEntry), true},
+ {typeof(ProjectMembership), true},
+ {typeof(Search), true}
+ };
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs
deleted file mode 100644
index b54db5b3..00000000
--- a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs
+++ /dev/null
@@ -1,422 +0,0 @@
-/*
- Copyright 2011 - 2025 Adrian Popescu
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-using System;
-using System.Net;
-using Redmine.Net.Api.Authentication;
-using Redmine.Net.Api.Exceptions;
-using Redmine.Net.Api.Extensions;
-using Redmine.Net.Api.Net;
-using Redmine.Net.Api.Net.WebClient;
-using Redmine.Net.Api.Serialization;
-
-namespace Redmine.Net.Api
-{
- ///
- ///
- ///
- public sealed class RedmineManagerOptionsBuilder
- {
- private enum ClientType
- {
- WebClient,
- HttpClient,
- }
- private ClientType _clientType = ClientType.WebClient;
-
- ///
- ///
- ///
- ///
- ///
- public RedmineManagerOptionsBuilder WithPageSize(int pageSize)
- {
- this.PageSize = pageSize;
- return this;
- }
-
- ///
- ///
- ///
- public int PageSize { get; private set; }
-
- ///
- ///
- ///
- ///
- ///
- public RedmineManagerOptionsBuilder WithHost(string baseAddress)
- {
- this.Host = baseAddress;
- return this;
- }
-
- ///
- ///
- ///
- public string Host { get; private set; }
-
- ///
- ///
- ///
- ///
- ///
- internal RedmineManagerOptionsBuilder WithSerializationType(MimeFormat mimeFormat)
- {
- this.SerializationType = mimeFormat == MimeFormat.Xml ? SerializationType.Xml : SerializationType.Json;
- return this;
- }
-
- ///
- ///
- ///
- ///
- ///
- public RedmineManagerOptionsBuilder WithSerializationType(SerializationType serializationType)
- {
- this.SerializationType = serializationType;
- return this;
- }
-
- ///
- /// Gets the current serialization type
- ///
- public SerializationType SerializationType { get; private set; }
-
- ///
- ///
- ///
- ///
- ///
- public RedmineManagerOptionsBuilder WithApiKeyAuthentication(string apiKey)
- {
- this.Authentication = new RedmineApiKeyAuthentication(apiKey);
- return this;
- }
-
- ///
- ///
- ///
- ///
- ///
- ///
- public RedmineManagerOptionsBuilder WithBasicAuthentication(string login, string password)
- {
- this.Authentication = new RedmineBasicAuthentication(login, password);
- return this;
- }
-
- ///
- ///
- ///
- public IRedmineAuthentication Authentication { get; private set; }
-
- ///
- ///
- ///
- ///
- ///
- public RedmineManagerOptionsBuilder WithWebClient(Func clientFunc)
- {
- _clientType = ClientType.WebClient;
- this.ClientFunc = clientFunc;
- return this;
- }
-
- ///
- ///
- ///
- public Func ClientFunc { get; private set; }
-
- ///
- ///
- ///
- ///
- ///
- [Obsolete("Use WithWebClientOptions(IRedmineWebClientOptions clientOptions) instead.")]
- public RedmineManagerOptionsBuilder WithWebClientOptions(IRedmineApiClientOptions clientOptions)
- {
- return WithWebClientOptions((IRedmineWebClientOptions)clientOptions);
- }
-
- ///
- ///
- ///
- ///
- ///
- public RedmineManagerOptionsBuilder WithWebClientOptions(IRedmineWebClientOptions clientOptions)
- {
- _clientType = ClientType.WebClient;
- this.WebClientOptions = clientOptions;
- return this;
- }
-
- ///
- ///
- ///
- [Obsolete("Use WebClientOptions instead.")]
- public IRedmineApiClientOptions ClientOptions
- {
- get => WebClientOptions;
- private set { }
- }
-
- ///
- ///
- ///
- public IRedmineWebClientOptions WebClientOptions { get; private set; }
-
- ///
- ///
- ///
- ///
- ///
- public RedmineManagerOptionsBuilder WithVersion(Version version)
- {
- this.Version = version;
- return this;
- }
-
- ///
- ///
- ///
- public Version Version { get; set; }
-
- internal RedmineManagerOptionsBuilder WithVerifyServerCert(bool verifyServerCert)
- {
- this.VerifyServerCert = verifyServerCert;
- return this;
- }
-
- ///
- ///
- ///
- public bool VerifyServerCert { get; private set; }
-
- ///
- ///
- ///
- ///
- internal RedmineManagerOptions Build()
- {
- const string defaultUserAgent = "Redmine.Net.Api.Net";
- var defaultDecompressionFormat =
- #if NETFRAMEWORK
- DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.None;
- #else
- DecompressionMethods.All;
- #endif
- #if NET45_OR_GREATER || NETCOREAPP
- WebClientOptions ??= _clientType switch
- {
- ClientType.WebClient => new RedmineWebClientOptions()
- {
- UserAgent = defaultUserAgent,
- DecompressionFormat = defaultDecompressionFormat,
- },
- _ => throw new ArgumentOutOfRangeException()
- };
- #else
- WebClientOptions ??= new RedmineWebClientOptions()
- {
- UserAgent = defaultUserAgent,
- DecompressionFormat = defaultDecompressionFormat,
- };
- #endif
- var baseAddress = CreateRedmineUri(Host, WebClientOptions.Scheme);
-
- var options = new RedmineManagerOptions()
- {
- BaseAddress = baseAddress,
- PageSize = PageSize > 0 ? PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE,
- VerifyServerCert = VerifyServerCert,
- Serializer = RedmineSerializerFactory.CreateSerializer(SerializationType),
- RedmineVersion = Version,
- Authentication = Authentication ?? new RedmineNoAuthentication(),
- WebClientOptions = WebClientOptions
- };
-
- return options;
- }
-
- private static readonly char[] DotCharArray = ['.'];
-
- internal static void EnsureDomainNameIsValid(string domainName)
- {
- if (domainName.IsNullOrWhiteSpace())
- {
- throw new RedmineException("Domain name cannot be null or empty.");
- }
-
- if (domainName.Length > 255)
- {
- throw new RedmineException("Domain name cannot be longer than 255 characters.");
- }
-
- var labels = domainName.Split(DotCharArray);
- if (labels.Length == 1)
- {
- throw new RedmineException("Domain name is not valid.");
- }
- foreach (var label in labels)
- {
- if (label.IsNullOrWhiteSpace() || label.Length > 63)
- {
- throw new RedmineException("Domain name must be between 1 and 63 characters.");
- }
-
- if (!char.IsLetterOrDigit(label[0]) || !char.IsLetterOrDigit(label[label.Length - 1]))
- {
- throw new RedmineException("Domain name starts or ends with a hyphen.");
- }
-
- for (var i = 0; i < label.Length; i++)
- {
- var c = label[i];
-
- if (!char.IsLetterOrDigit(c) && c != '-')
- {
- throw new RedmineException("Domain name contains an invalid character.");
- }
-
- if (c != '-')
- {
- continue;
- }
-
- if (i + 1 < label.Length && (c ^ label[i+1]) == 0)
- {
- throw new RedmineException("Domain name contains consecutive hyphens.");
- }
- }
- }
- }
-
- internal static Uri CreateRedmineUri(string host, string scheme = null)
- {
- if (host.IsNullOrWhiteSpace() || host.Equals("string.Empty", StringComparison.OrdinalIgnoreCase))
- {
- throw new RedmineException("The host is null or empty.");
- }
-
- if (!Uri.TryCreate(host, UriKind.Absolute, out var uri))
- {
- host = host.TrimEnd('/', '\\');
- EnsureDomainNameIsValid(host);
-
- if (!host.StartsWith(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || !host.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
- {
- host = $"{scheme ?? Uri.UriSchemeHttps}://{host}";
-
- if (!Uri.TryCreate(host, UriKind.Absolute, out uri))
- {
- throw new RedmineException("The host is not valid.");
- }
- }
- }
-
- if (!uri.IsWellFormedOriginalString())
- {
- throw new RedmineException("The host is not well-formed.");
- }
-
- scheme ??= Uri.UriSchemeHttps;
- var hasScheme = false;
- if (!uri.Scheme.IsNullOrWhiteSpace())
- {
- if (uri.Host.IsNullOrWhiteSpace() && uri.IsAbsoluteUri && !uri.IsFile)
- {
- if (uri.Scheme.Equals("localhost", StringComparison.OrdinalIgnoreCase))
- {
- int port = 0;
- var portAsString = uri.AbsolutePath.RemoveTrailingSlash();
- if (!portAsString.IsNullOrWhiteSpace())
- {
- int.TryParse(portAsString, out port);
- }
-
- var ub = new UriBuilder(scheme, "localhost", port);
- return ub.Uri;
- }
- }
- else
- {
- if (!IsSchemaHttpOrHttps(uri.Scheme))
- {
- throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported.");
- }
-
- hasScheme = true;
- }
- }
- else
- {
- if (!IsSchemaHttpOrHttps(scheme))
- {
- throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported.");
- }
- }
-
- var uriBuilder = new UriBuilder();
-
- if (uri.HostNameType == UriHostNameType.IPv6)
- {
- uriBuilder.Scheme = (hasScheme ? uri.Scheme : scheme ?? Uri.UriSchemeHttps);
- uriBuilder.Host = uri.Host;
- }
- else
- {
- if (uri.Authority.IsNullOrWhiteSpace())
- {
- if (uri.Port == -1)
- {
- if (int.TryParse(uri.LocalPath, out var port))
- {
- uriBuilder.Port = port;
- }
- }
-
- uriBuilder.Scheme = scheme ?? Uri.UriSchemeHttps;
- uriBuilder.Host = uri.Scheme;
- }
- else
- {
- uriBuilder.Scheme = uri.Scheme;
- uriBuilder.Port = int.TryParse(uri.LocalPath, out var port) ? port : uri.Port;
- uriBuilder.Host = uri.Host;
- if (!uri.LocalPath.IsNullOrWhiteSpace() && !uri.LocalPath.Contains("."))
- {
- uriBuilder.Path = uri.LocalPath;
- }
- }
- }
-
- try
- {
- return uriBuilder.Uri;
- }
- catch (Exception ex)
- {
- throw new RedmineException(ex.Message);
- }
- }
-
- private static bool IsSchemaHttpOrHttps(string scheme)
- {
- return scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeHttps;
- }
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/SearchFilterBuilder.cs b/src/redmine-net-api/SearchFilterBuilder.cs
index 856fb1a7..9b2b8c9c 100644
--- a/src/redmine-net-api/SearchFilterBuilder.cs
+++ b/src/redmine-net-api/SearchFilterBuilder.cs
@@ -17,6 +17,7 @@ limitations under the License.
using System;
using System.Collections.Specialized;
using Redmine.Net.Api.Extensions;
+using Redmine.Net.Api.Http.Extensions;
namespace Redmine.Net.Api
{
diff --git a/src/redmine-net-api/Serialization/IRedmineSerializer.cs b/src/redmine-net-api/Serialization/IRedmineSerializer.cs
index e6e064f4..eef94866 100644
--- a/src/redmine-net-api/Serialization/IRedmineSerializer.cs
+++ b/src/redmine-net-api/Serialization/IRedmineSerializer.cs
@@ -14,6 +14,8 @@ You may obtain a copy of the License at
limitations under the License.
*/
+using Redmine.Net.Api.Common;
+
namespace Redmine.Net.Api.Serialization
{
///
@@ -25,6 +27,11 @@ internal interface IRedmineSerializer
/// Gets the application format this serializer supports (e.g. "json", "xml").
///
string Format { get; }
+
+ ///
+ ///
+ ///
+ string ContentType { get; }
///
/// Serializes the specified object into a string.
diff --git a/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs
index 7d490315..8dc4e76e 100644
--- a/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs
+++ b/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs
@@ -18,9 +18,8 @@ limitations under the License.
using System.Collections.Generic;
using Newtonsoft.Json;
using Redmine.Net.Api.Exceptions;
-using Redmine.Net.Api.Serialization;
-namespace Redmine.Net.Api.Extensions
+namespace Redmine.Net.Api.Serialization.Json.Extensions
{
///
///
diff --git a/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs
index c921d706..f3df1629 100644
--- a/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs
+++ b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs
@@ -20,10 +20,11 @@ limitations under the License.
using System.Globalization;
using System.Text;
using Newtonsoft.Json;
-using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Common;
+using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Types;
-namespace Redmine.Net.Api.Extensions
+namespace Redmine.Net.Api.Serialization.Json.Extensions
{
///
///
@@ -72,7 +73,7 @@ public static void WriteIfNotDefaultOrNull(this JsonWriter writer, string ele
/// The property name.
public static void WriteBoolean(this JsonWriter writer, string elementName, bool value)
{
- writer.WriteProperty(elementName, value.ToString().ToLowerInv());
+ writer.WriteProperty(elementName, value.ToInvariantString());
}
///
@@ -84,7 +85,7 @@ public static void WriteBoolean(this JsonWriter writer, string elementName, bool
///
public static void WriteIdOrEmpty(this JsonWriter jsonWriter, string tag, IdentifiableName ident, string emptyValue = null)
{
- jsonWriter.WriteProperty(tag, ident != null ? ident.Id.ToString(CultureInfo.InvariantCulture) : emptyValue);
+ jsonWriter.WriteProperty(tag, ident != null ? ident.Id.ToInvariantString() : emptyValue);
}
///
@@ -212,7 +213,7 @@ public static void WriteArrayIds(this JsonWriter jsonWriter, string tag, IEnumer
foreach (var identifiableName in collection)
{
- sb.Append(identifiableName.Id.ToString(CultureInfo.InvariantCulture)).Append(',');
+ sb.Append(identifiableName.Id.ToInvariantString()).Append(',');
}
if (sb.Length > 1)
diff --git a/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs b/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs
index af82ab73..bf6b25b7 100644
--- a/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs
+++ b/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs
@@ -16,7 +16,7 @@ limitations under the License.
using Newtonsoft.Json;
-namespace Redmine.Net.Api.Serialization
+namespace Redmine.Net.Api.Serialization.Json
{
///
///
diff --git a/src/redmine-net-api/Serialization/Json/JsonObject.cs b/src/redmine-net-api/Serialization/Json/JsonObject.cs
index 612b2a10..38e70807 100644
--- a/src/redmine-net-api/Serialization/Json/JsonObject.cs
+++ b/src/redmine-net-api/Serialization/Json/JsonObject.cs
@@ -18,7 +18,7 @@ limitations under the License.
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
-namespace Redmine.Net.Api.Serialization
+namespace Redmine.Net.Api.Serialization.Json
{
///
///
diff --git a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs
index 41f66958..cd078537 100644
--- a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs
+++ b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs
@@ -19,130 +19,118 @@ limitations under the License.
using System.IO;
using System.Text;
using Newtonsoft.Json;
+using Redmine.Net.Api.Common;
using Redmine.Net.Api.Exceptions;
using Redmine.Net.Api.Extensions;
+using Redmine.Net.Api.Serialization.Json.Extensions;
-namespace Redmine.Net.Api.Serialization
+namespace Redmine.Net.Api.Serialization.Json
{
internal sealed class JsonRedmineSerializer : IRedmineSerializer
{
- public T Deserialize(string jsonResponse) where T : new()
+ private static void EnsureJsonSerializable()
{
- if (jsonResponse.IsNullOrWhiteSpace())
- {
- throw new ArgumentNullException(nameof(jsonResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'.");
- }
-
- var isJsonSerializable = typeof(IJsonSerializable).IsAssignableFrom(typeof(T));
-
- if (!isJsonSerializable)
+ if (!typeof(IJsonSerializable).IsAssignableFrom(typeof(T)))
{
throw new RedmineException($"Entity of type '{typeof(T)}' should implement IJsonSerializable.");
}
+ }
- using (var stringReader = new StringReader(jsonResponse))
- {
- using (var jsonReader = new JsonTextReader(stringReader))
- {
- var obj = Activator.CreateInstance();
+ public T Deserialize(string jsonResponse) where T : new()
+ {
+ SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(jsonResponse, nameof(jsonResponse), typeof(T));
- if (jsonReader.Read())
- {
- if (jsonReader.Read())
- {
- ((IJsonSerializable)obj).ReadJson(jsonReader);
- }
- }
+ EnsureJsonSerializable();
- return obj;
- }
+ using var stringReader = new StringReader(jsonResponse);
+ using var jsonReader = new JsonTextReader(stringReader);
+ var obj = Activator.CreateInstance();
+
+ if (jsonReader.Read() && jsonReader.Read())
+ {
+ ((IJsonSerializable)obj).ReadJson(jsonReader);
}
+
+ return obj;
}
public PagedResults DeserializeToPagedResults(string jsonResponse) where T : class, new()
{
- if (jsonResponse.IsNullOrWhiteSpace())
- {
- throw new ArgumentNullException(nameof(jsonResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'.");
- }
+ SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(jsonResponse, nameof(jsonResponse), typeof(T));
- using (var sr = new StringReader(jsonResponse))
+ using var sr = new StringReader(jsonResponse);
+ using var reader = new JsonTextReader(sr);
+ var total = 0;
+ var offset = 0;
+ var limit = 0;
+ List list = null;
+
+ while (reader.Read())
{
- using (var reader = new JsonTextReader(sr))
+ if (reader.TokenType != JsonToken.PropertyName)
+ {
+ continue;
+ }
+
+ switch (reader.Value)
{
- var total = 0;
- var offset = 0;
- var limit = 0;
- List list = null;
-
- while (reader.Read())
- {
- if (reader.TokenType != JsonToken.PropertyName) continue;
-
- switch (reader.Value)
- {
- case RedmineKeys.TOTAL_COUNT:
- total = reader.ReadAsInt32().GetValueOrDefault();
- break;
- case RedmineKeys.OFFSET:
- offset = reader.ReadAsInt32().GetValueOrDefault();
- break;
- case RedmineKeys.LIMIT:
- limit = reader.ReadAsInt32().GetValueOrDefault();
- break;
- default:
- list = reader.ReadAsCollection();
- break;
- }
- }
-
- return new PagedResults(list, total, offset, limit);
+ case RedmineKeys.TOTAL_COUNT:
+ total = reader.ReadAsInt32().GetValueOrDefault();
+ break;
+ case RedmineKeys.OFFSET:
+ offset = reader.ReadAsInt32().GetValueOrDefault();
+ break;
+ case RedmineKeys.LIMIT:
+ limit = reader.ReadAsInt32().GetValueOrDefault();
+ break;
+ default:
+ list = reader.ReadAsCollection();
+ break;
}
}
+
+ return new PagedResults(list, total, offset, limit);
}
#pragma warning disable CA1822
public int Count(string jsonResponse) where T : class, new()
{
- if (jsonResponse.IsNullOrWhiteSpace())
- {
- throw new ArgumentNullException(nameof(jsonResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'.");
- }
+ SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(jsonResponse, nameof(jsonResponse), typeof(T));
+
+ using var sr = new StringReader(jsonResponse);
+ using var reader = new JsonTextReader(sr);
+ var total = 0;
- using (var sr = new StringReader(jsonResponse))
+ while (reader.Read())
{
- using (var reader = new JsonTextReader(sr))
+ if (reader.TokenType != JsonToken.PropertyName)
{
- var total = 0;
-
- while (reader.Read())
- {
- if (reader.TokenType != JsonToken.PropertyName)
- {
- continue;
- }
-
- if (reader.Value is RedmineKeys.TOTAL_COUNT)
- {
- total = reader.ReadAsInt32().GetValueOrDefault();
- return total;
- }
- }
+ continue;
+ }
+ if (reader.Value is RedmineKeys.TOTAL_COUNT)
+ {
+ total = reader.ReadAsInt32().GetValueOrDefault();
return total;
}
}
+
+ return total;
}
#pragma warning restore CA1822
- public string Format { get; } = "json";
+ public string Format { get; } = RedmineConstants.JSON;
+
+ public string ContentType { get; } = RedmineConstants.CONTENT_TYPE_APPLICATION_JSON;
public string Serialize(T entity) where T : class
{
- if (entity == default(T))
+ if (entity == null)
{
- throw new ArgumentNullException(nameof(entity), $"Could not serialize null of type {typeof(T).Name}");
+ throw new RedmineSerializationException($"Could not serialize null of type {typeof(T).Name}", nameof(entity));
}
+
+ EnsureJsonSerializable();
if (entity is not IJsonSerializable jsonSerializable)
{
@@ -151,22 +139,18 @@ public string Serialize(T entity) where T : class
var stringBuilder = new StringBuilder();
- using (var sw = new StringWriter(stringBuilder))
- {
- using (var writer = new JsonTextWriter(sw))
- {
- writer.Formatting = Formatting.Indented;
- writer.DateFormatHandling = DateFormatHandling.IsoDateFormat;
+ using var sw = new StringWriter(stringBuilder);
+ using var writer = new JsonTextWriter(sw);
+ //writer.Formatting = Formatting.Indented;
+ writer.DateFormatHandling = DateFormatHandling.IsoDateFormat;
- jsonSerializable.WriteJson(writer);
+ jsonSerializable.WriteJson(writer);
- var json = stringBuilder.ToString();
+ var json = stringBuilder.ToString();
- stringBuilder.Length = 0;
+ stringBuilder.Length = 0;
- return json;
- }
- }
+ return json;
}
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Serialization/MimeFormatObsolete.cs b/src/redmine-net-api/Serialization/MimeFormatObsolete.cs
deleted file mode 100755
index 1bad6a4f..00000000
--- a/src/redmine-net-api/Serialization/MimeFormatObsolete.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
-Copyright 2011 - 2025 Adrian Popescu
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-using System;
-
-namespace Redmine.Net.Api
-{
- ///
- ///
- ///
- [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use SerializationType instead")]
- public enum MimeFormat
- {
- ///
- ///
- Xml,
- ///
- /// The json
- ///
- Json
- }
-}
\ No newline at end of file
diff --git a/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs b/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs
index a315ad44..6c71326e 100644
--- a/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs
+++ b/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs
@@ -15,6 +15,8 @@ limitations under the License.
*/
using System;
+using Redmine.Net.Api.Serialization.Json;
+using Redmine.Net.Api.Serialization.Xml;
namespace Redmine.Net.Api.Serialization;
@@ -23,6 +25,16 @@ namespace Redmine.Net.Api.Serialization;
///
internal static class RedmineSerializerFactory
{
+ ///
+ /// Creates an instance of an IRedmineSerializer based on the specified serialization type.
+ ///
+ /// The type of serialization, either Xml or Json.
+ ///
+ /// An instance of a serializer that implements the IRedmineSerializer interface.
+ ///
+ ///
+ /// Thrown when the specified serialization type is not supported.
+ ///
public static IRedmineSerializer CreateSerializer(SerializationType type)
{
return type switch
diff --git a/src/redmine-net-api/Serialization/SerializationHelper.cs b/src/redmine-net-api/Serialization/SerializationHelper.cs
index 225772aa..c54ce852 100644
--- a/src/redmine-net-api/Serialization/SerializationHelper.cs
+++ b/src/redmine-net-api/Serialization/SerializationHelper.cs
@@ -14,26 +14,45 @@ You may obtain a copy of the License at
limitations under the License.
*/
-using System.Globalization;
+using System;
+using Redmine.Net.Api.Exceptions;
+using Redmine.Net.Api.Extensions;
+using Redmine.Net.Api.Serialization.Json;
+using Redmine.Net.Api.Serialization.Xml;
namespace Redmine.Net.Api.Serialization
{
///
- ///
+ /// Provides helper methods for serializing user-related data for communication with the Redmine API.
///
internal static class SerializationHelper
{
///
- ///
+ /// Serializes the user ID into a format suitable for communication with the Redmine API,
+ /// based on the specified serializer type.
///
- ///
- ///
- ///
+ /// The ID of the user to be serialized.
+ /// The serializer used to format the user ID (e.g., XML or JSON).
+ /// A serialized representation of the user ID.
+ ///
+ /// Thrown when the provided serializer is not recognized or supported.
+ ///
public static string SerializeUserId(int userId, IRedmineSerializer redmineSerializer)
{
- return redmineSerializer is XmlRedmineSerializer
- ? $"{userId.ToString(CultureInfo.InvariantCulture)}"
- : $"{{\"user_id\":\"{userId.ToString(CultureInfo.InvariantCulture)}\"}}";
+ return redmineSerializer switch
+ {
+ XmlRedmineSerializer => $"{userId.ToInvariantString()}",
+ JsonRedmineSerializer => $"{{\"user_id\":\"{userId.ToInvariantString()}\"}}",
+ _ => throw new ArgumentOutOfRangeException(nameof(redmineSerializer), redmineSerializer, null)
+ };
+ }
+
+ public static void EnsureDeserializationInputIsNotNullOrWhiteSpace(string input, string paramName, Type type)
+ {
+ if (input.IsNullOrWhiteSpace())
+ {
+ throw new RedmineSerializationException($"Could not deserialize null or empty input for type '{type.Name}'.", paramName);
+ }
}
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Serialization/SerializationType.cs b/src/redmine-net-api/Serialization/SerializationType.cs
index decd824c..3830d3fe 100644
--- a/src/redmine-net-api/Serialization/SerializationType.cs
+++ b/src/redmine-net-api/Serialization/SerializationType.cs
@@ -17,15 +17,17 @@ limitations under the License.
namespace Redmine.Net.Api.Serialization
{
///
- ///
+ /// Specifies the serialization types supported by the Redmine API.
///
public enum SerializationType
{
///
+ /// The XML format.
///
Xml,
+
///
- /// The json
+ /// The JSON format.
///
Json
}
diff --git a/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs b/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs
index 45f17b12..47cef30d 100644
--- a/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs
+++ b/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs
@@ -15,11 +15,11 @@ limitations under the License.
*/
using System;
-using System.Globalization;
using System.Text;
using System.Xml.Serialization;
+using Redmine.Net.Api.Extensions;
-namespace Redmine.Net.Api.Serialization
+namespace Redmine.Net.Api.Serialization.Xml
{
///
/// The CacheKeyFactory extracts a unique signature
@@ -46,11 +46,11 @@ public static string Create(Type type, XmlAttributeOverrides overrides, Type[] t
var keyBuilder = new StringBuilder();
keyBuilder.Append(type.FullName);
keyBuilder.Append( "??" );
- keyBuilder.Append(overrides?.GetHashCode().ToString(CultureInfo.InvariantCulture));
+ keyBuilder.Append(overrides?.GetHashCode().ToInvariantString());
keyBuilder.Append( "??" );
keyBuilder.Append(GetTypeArraySignature(types));
keyBuilder.Append("??");
- keyBuilder.Append(root?.GetHashCode().ToString(CultureInfo.InvariantCulture));
+ keyBuilder.Append(root?.GetHashCode().ToInvariantString());
keyBuilder.Append("??");
keyBuilder.Append(defaultNamespace);
diff --git a/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs
index 8a57a1e6..3342ed05 100644
--- a/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs
+++ b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs
@@ -20,9 +20,9 @@ limitations under the License.
using System.IO;
using System.Xml;
using System.Xml.Serialization;
-using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Extensions;
-namespace Redmine.Net.Api.Extensions
+namespace Redmine.Net.Api.Serialization.Xml.Extensions
{
///
///
diff --git a/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs
index cf33cb1c..b641af40 100644
--- a/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs
+++ b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs
@@ -20,9 +20,11 @@ limitations under the License.
using System.Globalization;
using System.Xml;
using System.Xml.Serialization;
+using Redmine.Net.Api.Common;
+using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Types;
-namespace Redmine.Net.Api.Extensions
+namespace Redmine.Net.Api.Serialization.Xml.Extensions
{
///
///
@@ -47,7 +49,7 @@ public static void WriteIdIfNotNull(this XmlWriter writer, string elementName, I
{
if (identifiableName != null)
{
- writer.WriteElementString(elementName, identifiableName.Id.ToString(CultureInfo.InvariantCulture));
+ writer.WriteElementString(elementName, identifiableName.Id.ToInvariantString());
}
}
@@ -160,6 +162,39 @@ public static void WriteArray(this XmlWriter writer, string elementName, IEnumer
writer.WriteEndElement();
}
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection, string root, string defaultNamespace = null)
+ {
+ if (collection == null)
+ {
+ return;
+ }
+
+ var type = typeof(T);
+ writer.WriteStartElement(elementName);
+ writer.WriteAttributeString("type", "array");
+
+ var rootAttribute = new XmlRootAttribute(root);
+
+ var serializer = new XmlSerializer(type, XmlAttributeOverrides, EmptyTypeArray, rootAttribute,
+ defaultNamespace);
+
+ foreach (var item in collection)
+ {
+ serializer.Serialize(writer, item);
+ }
+
+ writer.WriteEndElement();
+ }
///
/// Writes the list elements.
@@ -232,7 +267,7 @@ public static void WriteArrayStringElement(this XmlWriter writer, string element
///
public static void WriteIdOrEmpty(this XmlWriter writer, string elementName, IdentifiableName ident)
{
- writer.WriteElementString(elementName, ident != null ? ident.Id.ToString(CultureInfo.InvariantCulture) : string.Empty);
+ writer.WriteElementString(elementName, ident != null ? ident.Id.ToInvariantString() : string.Empty);
}
///
@@ -267,7 +302,7 @@ public static void WriteIfNotDefaultOrNull(this XmlWriter writer, string elem
/// The tag.
public static void WriteBoolean(this XmlWriter writer, string elementName, bool value)
{
- writer.WriteElementString(elementName, value.ToString().ToLowerInv());
+ writer.WriteElementString(elementName, value.ToInvariantString());
}
///
@@ -285,7 +320,7 @@ public static void WriteValueOrEmpty(this XmlWriter writer, string elementNam
}
else
{
- writer.WriteElementString(elementName, val.Value.ToString().ToLowerInv());
+ writer.WriteElementString(elementName, val.Value.ToInvariantString());
}
}
diff --git a/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs b/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs
index bfb5b416..a11e2d33 100644
--- a/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs
+++ b/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs
@@ -17,7 +17,7 @@ limitations under the License.
using System;
using System.Xml.Serialization;
-namespace Redmine.Net.Api.Serialization
+namespace Redmine.Net.Api.Serialization.Xml
{
internal interface IXmlSerializerCache
{
diff --git a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs
index 253543c7..e86111df 100644
--- a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs
+++ b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs
@@ -18,15 +18,23 @@ limitations under the License.
using System.IO;
using System.Xml;
using System.Xml.Serialization;
+using Redmine.Net.Api.Common;
using Redmine.Net.Api.Exceptions;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
-namespace Redmine.Net.Api.Serialization
+namespace Redmine.Net.Api.Serialization.Xml
{
internal sealed class XmlRedmineSerializer : IRedmineSerializer
{
-
+ private static void EnsureXmlSerializable()
+ {
+ if (!typeof(IXmlSerializable).IsAssignableFrom(typeof(T)))
+ {
+ throw new RedmineException($"Entity of type '{typeof(T)}' should implement ${nameof(IXmlSerializable)}.");
+ }
+ }
public XmlRedmineSerializer() : this(new XmlWriterSettings
{
OmitXmlDeclaration = true
@@ -79,6 +87,8 @@ public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings)
#pragma warning restore CA1822
public string Format => RedmineConstants.XML;
+
+ public string ContentType { get; } = RedmineConstants.CONTENT_TYPE_APPLICATION_XML;
public string Serialize(T entity) where T : class
{
@@ -101,39 +111,32 @@ public string Serialize(T entity) where T : class
///
private static PagedResults XmlDeserializeList(string xmlResponse, bool onlyCount) where T : class, new()
{
- if (xmlResponse.IsNullOrWhiteSpace())
+ SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(xmlResponse, nameof(xmlResponse), typeof(T));
+
+ using var stringReader = new StringReader(xmlResponse);
+ using var xmlReader = XmlTextReaderBuilder.Create(stringReader);
+ while (xmlReader.NodeType == XmlNodeType.None || xmlReader.NodeType == XmlNodeType.XmlDeclaration)
{
- throw new ArgumentNullException(nameof(xmlResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'.");
+ xmlReader.Read();
}
- using (var stringReader = new StringReader(xmlResponse))
- {
- using (var xmlReader = XmlTextReaderBuilder.Create(stringReader))
- {
- while (xmlReader.NodeType == XmlNodeType.None || xmlReader.NodeType == XmlNodeType.XmlDeclaration)
- {
- xmlReader.Read();
- }
-
- var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT);
+ var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT);
- if (onlyCount)
- {
- return new PagedResults(null, totalItems, 0, 0);
- }
+ if (onlyCount)
+ {
+ return new PagedResults(null, totalItems, 0, 0);
+ }
- var offset = xmlReader.ReadAttributeAsInt(RedmineKeys.OFFSET);
- var limit = xmlReader.ReadAttributeAsInt(RedmineKeys.LIMIT);
- var result = xmlReader.ReadElementContentAsCollection();
-
- if (totalItems == 0 && result?.Count > 0)
- {
- totalItems = result.Count;
- }
+ var offset = xmlReader.ReadAttributeAsInt(RedmineKeys.OFFSET);
+ var limit = xmlReader.ReadAttributeAsInt(RedmineKeys.LIMIT);
+ var result = xmlReader.ReadElementContentAsCollection();
- return new PagedResults(result, totalItems, offset, limit);
- }
+ if (totalItems == 0 && result?.Count > 0)
+ {
+ totalItems = result.Count;
}
+
+ return new PagedResults(result, totalItems, offset, limit);
}
///
@@ -148,22 +151,18 @@ public string Serialize(T entity) where T : class
// ReSharper disable once InconsistentNaming
private string ToXML(T entity) where T : class
{
- if (entity == default(T))
+ if (entity == null)
{
throw new ArgumentNullException(nameof(entity), $"Could not serialize null of type {typeof(T).Name}");
}
- using (var stringWriter = new StringWriter())
- {
- using (var xmlWriter = XmlWriter.Create(stringWriter, _xmlWriterSettings))
- {
- var serializer = new XmlSerializer(typeof(T));
+ using var stringWriter = new StringWriter();
+ using var xmlWriter = XmlWriter.Create(stringWriter, _xmlWriterSettings);
+ var serializer = new XmlSerializer(typeof(T));
- serializer.Serialize(xmlWriter, entity);
+ serializer.Serialize(xmlWriter, entity);
- return stringWriter.ToString();
- }
- }
+ return stringWriter.ToString();
}
///
@@ -181,27 +180,20 @@ private string ToXML(T entity) where T : class
// ReSharper disable once InconsistentNaming
private static TOut XmlDeserializeEntity(string xml) where TOut : new()
{
- if (xml.IsNullOrWhiteSpace())
- {
- throw new ArgumentNullException(nameof(xml), $"Could not deserialize null or empty input for type '{typeof(TOut).Name}'.");
- }
-
- using (var textReader = new StringReader(xml))
- {
- using (var xmlReader = XmlTextReaderBuilder.Create(textReader))
- {
- var serializer = new XmlSerializer(typeof(TOut));
+ SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(xml, nameof(xml), typeof(TOut));
- var entity = serializer.Deserialize(xmlReader);
+ using var textReader = new StringReader(xml);
+ using var xmlReader = XmlTextReaderBuilder.Create(textReader);
+ var serializer = new XmlSerializer(typeof(TOut));
- if (entity is TOut t)
- {
- return t;
- }
+ var entity = serializer.Deserialize(xmlReader);
- return default;
- }
+ if (entity is TOut t)
+ {
+ return t;
}
+
+ return default;
}
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs b/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs
index 9a63c8d0..fcd977ba 100644
--- a/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs
+++ b/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs
@@ -19,7 +19,7 @@ limitations under the License.
using System.Diagnostics;
using System.Xml.Serialization;
-namespace Redmine.Net.Api.Serialization
+namespace Redmine.Net.Api.Serialization.Xml
{
///
///
diff --git a/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs b/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs
index dc83be34..6a01e8ab 100644
--- a/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs
+++ b/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs
@@ -17,7 +17,7 @@ limitations under the License.
using System.IO;
using System.Xml;
-namespace Redmine.Net.Api.Internals
+namespace Redmine.Net.Api.Serialization.Xml
{
///
///
diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs
index 49b6476c..56afc0c5 100644
--- a/src/redmine-net-api/Types/Attachment.cs
+++ b/src/redmine-net-api/Types/Attachment.cs
@@ -16,20 +16,22 @@ limitations under the License.
using System;
using System.Diagnostics;
-using System.Globalization;
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
/// Availability 1.3
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.ATTACHMENT)]
public sealed class Attachment :
Identifiable
@@ -123,8 +125,8 @@ public override void ReadXml(XmlReader reader)
///
public override void WriteXml(XmlWriter writer)
{
- writer.WriteElementString(RedmineKeys.DESCRIPTION, Description);
writer.WriteElementString(RedmineKeys.FILE_NAME, FileName);
+ writer.WriteElementString(RedmineKeys.DESCRIPTION, Description);
}
#endregion
diff --git a/src/redmine-net-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs
index 21655fec..c475d531 100644
--- a/src/redmine-net-api/Types/Attachments.cs
+++ b/src/redmine-net-api/Types/Attachments.cs
@@ -17,7 +17,9 @@ limitations under the License.
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
+using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
namespace Redmine.Net.Api.Types
{
@@ -43,7 +45,7 @@ public void WriteJson(JsonWriter writer)
writer.WriteStartArray();
foreach (var item in this)
{
- writer.WritePropertyName(item.Key.ToString(CultureInfo.InvariantCulture));
+ writer.WritePropertyName(item.Key.ToInvariantString());
item.Value.WriteJson(writer);
}
writer.WriteEndArray();
diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs
index be873273..adfff4fb 100644
--- a/src/redmine-net-api/Types/ChangeSet.cs
+++ b/src/redmine-net-api/Types/ChangeSet.cs
@@ -24,13 +24,15 @@ limitations under the License.
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.CHANGE_SET)]
public sealed class ChangeSet : IXmlSerializable, IJsonSerializable, IEquatable
,ICloneable
diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs
index c99a1490..0c0ca47f 100644
--- a/src/redmine-net-api/Types/CustomField.cs
+++ b/src/redmine-net-api/Types/CustomField.cs
@@ -17,19 +17,20 @@ limitations under the License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.CUSTOM_FIELD)]
public sealed class CustomField : IdentifiableName, IEquatable
{
@@ -97,17 +98,17 @@ public sealed class CustomField : IdentifiableName, IEquatable
///
///
///
- public IList PossibleValues { get; internal set; }
+ public List PossibleValues { get; internal set; }
///
///
///
- public IList Trackers { get; internal set; }
+ public List Trackers { get; internal set; }
///
///
///
- public IList Roles { get; internal set; }
+ public List Roles { get; internal set; }
#endregion
#region Implementation of IXmlSerializable
diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs
index 4a8854f7..56edabd7 100644
--- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs
+++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs
@@ -22,13 +22,14 @@ limitations under the License.
using Newtonsoft.Json;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.POSSIBLE_VALUE)]
public sealed class CustomFieldPossibleValue : IXmlSerializable, IJsonSerializable, IEquatable
{
diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs
index bfffbc71..7b1b20a9 100644
--- a/src/redmine-net-api/Types/CustomFieldRole.cs
+++ b/src/redmine-net-api/Types/CustomFieldRole.cs
@@ -23,7 +23,7 @@ namespace Redmine.Net.Api.Types
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.ROLE)]
public sealed class CustomFieldRole : IdentifiableName
{
diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs
index 32a70959..1dacf6b8 100644
--- a/src/redmine-net-api/Types/CustomFieldValue.cs
+++ b/src/redmine-net-api/Types/CustomFieldValue.cs
@@ -22,13 +22,14 @@ limitations under the License.
using Newtonsoft.Json;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.VALUE)]
public class CustomFieldValue :
IXmlSerializable
diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs
index b4738792..4f69af20 100644
--- a/src/redmine-net-api/Types/Detail.cs
+++ b/src/redmine-net-api/Types/Detail.cs
@@ -22,13 +22,14 @@ limitations under the License.
using Newtonsoft.Json;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.DETAIL)]
public sealed class Detail :
IXmlSerializable
diff --git a/src/redmine-net-api/Types/DocumentCategory.cs b/src/redmine-net-api/Types/DocumentCategory.cs
index 4e01bc96..c8935cce 100644
--- a/src/redmine-net-api/Types/DocumentCategory.cs
+++ b/src/redmine-net-api/Types/DocumentCategory.cs
@@ -16,19 +16,19 @@ limitations under the License.
using System;
using System.Diagnostics;
-using System.Globalization;
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Json.Extensions;
namespace Redmine.Net.Api.Types
{
///
/// Availability 2.2
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.DOCUMENT_CATEGORY)]
public sealed class DocumentCategory : IdentifiableName, IEquatable
{
diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs
index f5285573..f8632707 100644
--- a/src/redmine-net-api/Types/Error.cs
+++ b/src/redmine-net-api/Types/Error.cs
@@ -22,13 +22,14 @@ limitations under the License.
using Newtonsoft.Json;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.ERROR)]
public sealed class Error : IXmlSerializable, IJsonSerializable, IEquatable
{
diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs
index f4f09e30..491f41d9 100644
--- a/src/redmine-net-api/Types/File.cs
+++ b/src/redmine-net-api/Types/File.cs
@@ -24,13 +24,16 @@ limitations under the License.
using System.Xml.Serialization;
using Newtonsoft.Json;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.FILE)]
public sealed class File : Identifiable
{
diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs
index 27bc3773..8d3d01d1 100644
--- a/src/redmine-net-api/Types/Group.cs
+++ b/src/redmine-net-api/Types/Group.cs
@@ -20,16 +20,20 @@ limitations under the License.
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
+using Redmine.Net.Api.Common;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
/// Availability 2.1
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.GROUP)]
public sealed class Group : IdentifiableName, IEquatable
{
@@ -51,19 +55,19 @@ public Group(string name)
///
/// Represents the group's users.
///
- public IList Users { get; set; }
+ public List Users { get; set; }
///
/// Gets or sets the custom fields.
///
/// The custom fields.
- public IList CustomFields { get; internal set; }
+ public List CustomFields { get; internal set; }
///
/// Gets or sets the custom fields.
///
/// The custom fields.
- public IList Memberships { get; internal set; }
+ public List Memberships { get; internal set; }
#endregion
#region Implementation of IXmlSerializable
diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs
index 598e9462..4264f82d 100644
--- a/src/redmine-net-api/Types/GroupUser.cs
+++ b/src/redmine-net-api/Types/GroupUser.cs
@@ -17,6 +17,7 @@ limitations under the License.
using System.Diagnostics;
using System.Globalization;
using System.Xml.Serialization;
+using Redmine.Net.Api.Common;
using Redmine.Net.Api.Extensions;
namespace Redmine.Net.Api.Types
@@ -24,7 +25,7 @@ namespace Redmine.Net.Api.Types
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.USER)]
public sealed class GroupUser : IdentifiableName, IValue
{
@@ -32,7 +33,7 @@ public sealed class GroupUser : IdentifiableName, IValue
///
///
///
- public string Value => Id.ToString(CultureInfo.InvariantCulture);
+ public string Value => Id.ToInvariantString();
#endregion
///
diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs
index 02307d1d..42d789a7 100644
--- a/src/redmine-net-api/Types/Identifiable.cs
+++ b/src/redmine-net-api/Types/Identifiable.cs
@@ -16,7 +16,6 @@ limitations under the License.
using System;
using System.Diagnostics;
-using System.Globalization;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
@@ -24,7 +23,7 @@ limitations under the License.
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
-using NotImplementedException = System.NotImplementedException;
+using Redmine.Net.Api.Serialization.Json;
namespace Redmine.Net.Api.Types
{
@@ -32,7 +31,7 @@ namespace Redmine.Net.Api.Types
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
public abstract class Identifiable : IXmlSerializable, IJsonSerializable, IEquatable
, ICloneable>
where T : Identifiable
diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs
index 2a985283..f320426f 100644
--- a/src/redmine-net-api/Types/IdentifiableName.cs
+++ b/src/redmine-net-api/Types/IdentifiableName.cs
@@ -21,13 +21,15 @@ limitations under the License.
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
public class IdentifiableName : Identifiable
, ICloneable
{
@@ -38,7 +40,19 @@ public class IdentifiableName : Identifiable
///
public static T Create(int id) where T: IdentifiableName, new()
{
- var t = new T (){Id = id};
+ var t = new T
+ {
+ Id = id
+ };
+ return t;
+ }
+
+ internal static T Create(int id, string name) where T: IdentifiableName, new()
+ {
+ var t = new T
+ {
+ Id = id, Name = name
+ };
return t;
}
@@ -112,7 +126,7 @@ public override void ReadXml(XmlReader reader)
///
public override void WriteXml(XmlWriter writer)
{
- writer.WriteAttributeString(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture));
+ writer.WriteAttributeString(RedmineKeys.ID, Id.ToInvariantString());
writer.WriteAttributeString(RedmineKeys.NAME, Name);
}
@@ -219,7 +233,24 @@ public override int GetHashCode()
return !Equals(left, right);
}
#endregion
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static implicit operator string(IdentifiableName identifiableName) => FromIdentifiableName(identifiableName);
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static string FromIdentifiableName(IdentifiableName identifiableName)
+ {
+ return identifiableName?.Id.ToInvariantString();
+ }
+
///
///
///
diff --git a/src/redmine-net-api/Types/Include.cs b/src/redmine-net-api/Types/Include.cs
new file mode 100644
index 00000000..1c8b2d9c
--- /dev/null
+++ b/src/redmine-net-api/Types/Include.cs
@@ -0,0 +1,147 @@
+namespace Redmine.Net.Api.Types;
+
+///
+///
+///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+"Design",
+"CA1034:Nested types should not be visible",
+Justification = "Deliberately exposed")]
+
+public static class Include
+{
+ ///
+ ///
+ ///
+ public static class Group
+ {
+ ///
+ ///
+ ///
+ public const string Users = RedmineKeys.USERS;
+
+ ///
+ /// Adds extra information about user's memberships and roles on the projects
+ ///
+ public const string Memberships = RedmineKeys.MEMBERSHIPS;
+ }
+
+ ///
+ /// Associated data that can be retrieved
+ ///
+ public static class Issue
+ {
+ ///
+ /// Specifies whether to include child issues.
+ /// This parameter is applicable when retrieving details for a specific issue.
+ /// Corresponds to the Redmine API include parameter: children.
+ ///
+ public const string Children = RedmineKeys.CHILDREN;
+
+ ///
+ /// Specifies whether to include attachments.
+ /// This parameter is applicable when retrieving a list of issues or details for a specific issue.
+ /// Corresponds to the Redmine API include parameter: attachments.
+ ///
+ public const string Attachments = RedmineKeys.ATTACHMENTS;
+
+ ///
+ /// Specifies whether to include issue relations.
+ /// This parameter is applicable when retrieving a list of issues or details for a specific issue.
+ /// Corresponds to the Redmine API include parameter: relations.
+ ///
+ public const string Relations = RedmineKeys.RELATIONS;
+
+ ///
+ /// Specifies whether to include associated changesets.
+ /// This parameter is applicable when retrieving details for a specific issue.
+ /// Corresponds to the Redmine API include parameter: changesets.
+ ///
+ public const string Changesets = RedmineKeys.CHANGE_SETS;
+
+ ///
+ /// Specifies whether to include journal entries (notes and history).
+ /// This parameter is applicable when retrieving details for a specific issue.
+ /// Corresponds to the Redmine API include parameter: journals.
+ ///
+ public const string Journals = RedmineKeys.JOURNALS;
+
+ ///
+ /// Specifies whether to include watchers of the issue.
+ /// This parameter is applicable when retrieving details for a specific issue.
+ /// Corresponds to the Redmine API include parameter: watchers.
+ ///
+ public const string Watchers = RedmineKeys.WATCHERS;
+
+ ///
+ /// Specifies whether to include allowed statuses of the issue.
+ /// This parameter is applicable when retrieving details for a specific issue.
+ /// Corresponds to the Redmine API include parameter: watchers.
+ /// Since 5.0.x, Returns the available allowed statuses (the same values as provided in the issue edit form) based on:
+ /// the issue's current tracker, the issue's current status, and the member's role (the defined workflow);
+ /// the existence of any open subtask(s);
+ /// the existence of any open blocking issue(s);
+ /// the existence of a closed parent issue.
+ ///
+ public const string AllowedStatuses = RedmineKeys.ALLOWED_STATUSES;
+ }
+
+ ///
+ ///
+ ///
+ public static class Project
+ {
+ ///
+ ///
+ ///
+ public const string Trackers = RedmineKeys.TRACKERS;
+
+ ///
+ /// since 2.6.0
+ ///
+ public const string EnabledModules = RedmineKeys.ENABLED_MODULES;
+
+ ///
+ ///
+ ///
+ public const string IssueCategories = RedmineKeys.ISSUE_CATEGORIES;
+
+ ///
+ /// since 3.4.0
+ ///
+ public const string TimeEntryActivities = RedmineKeys.TIME_ENTRY_ACTIVITIES;
+
+ ///
+ /// since 4.2.0
+ ///
+ public const string IssueCustomFields = RedmineKeys.ISSUE_CUSTOM_FIELDS;
+ }
+
+ ///
+ ///
+ ///
+ public static class User
+ {
+ ///
+ /// Adds extra information about user's memberships and roles on the projects
+ ///
+ public const string Memberships = RedmineKeys.MEMBERSHIPS;
+
+ ///
+ /// Adds extra information about user's groups
+ /// added in 2.1
+ ///
+ public const string Groups = RedmineKeys.GROUPS;
+ }
+
+ ///
+ ///
+ ///
+ public static class WikiPage
+ {
+ ///
+ ///
+ ///
+ public const string Attachments = RedmineKeys.ATTACHMENTS;
+ }
+}
\ No newline at end of file
diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs
index 14f55a6f..1298f214 100644
--- a/src/redmine-net-api/Types/Issue.cs
+++ b/src/redmine-net-api/Types/Issue.cs
@@ -21,9 +21,13 @@ limitations under the License.
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
+using Redmine.Net.Api.Common;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
@@ -36,7 +40,7 @@ namespace Redmine.Net.Api.Types
/// Possible values: children, attachments, relations, changesets and journals. To fetch multiple associations use comma (e.g ?include=relations,journals).
/// See Issue journals for more information.
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.ISSUE)]
public sealed class Issue :
Identifiable
@@ -56,7 +60,7 @@ public sealed class Issue :
public IdentifiableName Tracker { get; set; }
///
- /// Gets or sets the status.Possible values: open, closed, * to get open and closed issues, status id
+ /// Gets or sets the status. Possible values: open, closed, * to get open and closed issues, status id
///
/// The status.
public IssueStatus Status { get; set; }
@@ -133,7 +137,7 @@ public sealed class Issue :
/// Gets or sets the custom fields.
///
/// The custom fields.
- public IList CustomFields { get; set; }
+ public List CustomFields { get; set; }
///
/// Gets or sets the created on.
@@ -208,7 +212,7 @@ public sealed class Issue :
///
/// The journals.
///
- public IList Journals { get; set; }
+ public List Journals { get; set; }
///
/// Gets or sets the change sets.
@@ -216,7 +220,7 @@ public sealed class Issue :
///
/// The change sets.
///
- public IList ChangeSets { get; set; }
+ public List ChangeSets { get; set; }
///
/// Gets or sets the attachments.
@@ -224,7 +228,7 @@ public sealed class Issue :
///
/// The attachments.
///
- public IList Attachments { get; set; }
+ public List Attachments { get; set; }
///
/// Gets or sets the issue relations.
@@ -232,7 +236,7 @@ public sealed class Issue :
///
/// The issue relations.
///
- public IList Relations { get; set; }
+ public List Relations { get; set; }
///
/// Gets or sets the issue children.
@@ -241,7 +245,7 @@ public sealed class Issue :
/// The issue children.
/// NOTE: Only Id, tracker and subject are filled.
///
- public IList Children { get; set; }
+ public List Children { get; set; }
///
/// Gets or sets the attachments.
@@ -249,12 +253,12 @@ public sealed class Issue :
///
/// The attachment.
///
- public IList Uploads { get; set; }
+ public List Uploads { get; set; }
///
///
///
- public IList Watchers { get; set; }
+ public List Watchers { get; set; }
///
///
@@ -334,7 +338,7 @@ public override void WriteXml(XmlWriter writer)
}
writer.WriteElementString(RedmineKeys.DESCRIPTION, Description);
- writer.WriteElementString(RedmineKeys.IS_PRIVATE, IsPrivate.ToString(CultureInfo.InvariantCulture).ToLowerInv());
+ writer.WriteElementString(RedmineKeys.IS_PRIVATE, IsPrivate.ToInvariantString());
writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project);
writer.WriteIdIfNotNull(RedmineKeys.PRIORITY_ID, Priority);
@@ -344,7 +348,6 @@ public override void WriteXml(XmlWriter writer)
writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, AssignedTo);
writer.WriteIdIfNotNull(RedmineKeys.PARENT_ISSUE_ID, ParentIssue);
writer.WriteIdIfNotNull(RedmineKeys.FIXED_VERSION_ID, FixedVersion);
-
writer.WriteValueOrEmpty(RedmineKeys.ESTIMATED_HOURS, EstimatedHours);
writer.WriteIfNotDefaultOrNull(RedmineKeys.DONE_RATIO, DoneRatio);
@@ -452,12 +455,12 @@ public override void WriteJson(JsonWriter writer)
if (DoneRatio != null)
{
- writer.WriteProperty(RedmineKeys.DONE_RATIO, DoneRatio.Value.ToString(CultureInfo.InvariantCulture));
+ writer.WriteProperty(RedmineKeys.DONE_RATIO, DoneRatio.Value.ToInvariantString());
}
if (SpentHours != null)
{
- writer.WriteProperty(RedmineKeys.SPENT_HOURS, SpentHours.Value.ToString(CultureInfo.InvariantCulture));
+ writer.WriteProperty(RedmineKeys.SPENT_HOURS, SpentHours.Value.ToInvariantString());
}
writer.WriteArray(RedmineKeys.UPLOADS, Uploads);
@@ -645,11 +648,12 @@ public IdentifiableName AsParent()
return IdentifiableName.Create(Id);
}
-
///
- ///
+ /// Provides a string representation of the object for use in debugging.
///
- ///
+ ///
+ /// A string that represents the object, formatted for debugging purposes.
+ ///
private string DebuggerDisplay => $"[Issue:Id={Id.ToInvariantString()}, Status={Status?.Name}, Priority={Priority?.Name}, DoneRatio={DoneRatio?.ToString("F", CultureInfo.InvariantCulture)},IsPrivate={IsPrivate.ToInvariantString()}]";
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Types/IssueAllowedStatus.cs b/src/redmine-net-api/Types/IssueAllowedStatus.cs
index 9a47811a..1fa60dba 100644
--- a/src/redmine-net-api/Types/IssueAllowedStatus.cs
+++ b/src/redmine-net-api/Types/IssueAllowedStatus.cs
@@ -20,13 +20,15 @@ limitations under the License.
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.STATUS)]
public sealed class IssueAllowedStatus : IdentifiableName
{
diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs
index 39c5781c..1dbc3ca5 100644
--- a/src/redmine-net-api/Types/IssueCategory.cs
+++ b/src/redmine-net-api/Types/IssueCategory.cs
@@ -21,13 +21,16 @@ limitations under the License.
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
/// Availability 1.3
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.ISSUE_CATEGORY)]
public sealed class IssueCategory : Identifiable
{
@@ -90,7 +93,7 @@ public override void ReadXml(XmlReader reader)
///
public override void WriteXml(XmlWriter writer)
{
- writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project);
+ // writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project);
writer.WriteElementString(RedmineKeys.NAME, Name);
writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, AssignTo);
}
diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs
index b6d3fcf1..a6e9adfe 100644
--- a/src/redmine-net-api/Types/IssueChild.cs
+++ b/src/redmine-net-api/Types/IssueChild.cs
@@ -22,13 +22,14 @@ limitations under the License.
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Json.Extensions;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.ISSUE)]
public sealed class IssueChild : Identifiable
,ICloneable
diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs
index da8935c9..0333e832 100644
--- a/src/redmine-net-api/Types/IssueCustomField.cs
+++ b/src/redmine-net-api/Types/IssueCustomField.cs
@@ -21,15 +21,18 @@ limitations under the License.
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
+using Redmine.Net.Api.Common;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.CUSTOM_FIELD)]
public sealed class IssueCustomField :
IdentifiableName
@@ -41,7 +44,7 @@ public sealed class IssueCustomField :
/// Gets or sets the value.
///
/// The value.
- public IList Values { get; set; }
+ public List Values { get; set; }
///
///
@@ -113,7 +116,7 @@ public override void WriteXml(XmlWriter writer)
var itemsCount = Values.Count;
- writer.WriteAttributeString(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture));
+ writer.WriteAttributeString(RedmineKeys.ID, Id.ToInvariantString());
Multiple = itemsCount > 1;
@@ -307,7 +310,7 @@ public override int GetHashCode()
///
///
///
- public string Value => Id.ToString(CultureInfo.InvariantCulture);
+ public string Value => Id.ToInvariantString();
#endregion
@@ -326,5 +329,53 @@ public static string GetValue(object item)
///
///
private string DebuggerDisplay => $"[IssueCustomField: Id={Id.ToInvariantString()}, Name={Name}, Multiple={Multiple.ToInvariantString()}]";
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IssueCustomField CreateSingle(int id, string name, string value)
+ {
+ return new IssueCustomField
+ {
+ Id = id,
+ Name = name,
+ Values = [new CustomFieldValue { Info = value }]
+ };
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IssueCustomField CreateMultiple(int id, string name, string[] values)
+ {
+ var isf = new IssueCustomField
+ {
+ Id = id,
+ Name = name,
+ Multiple = true,
+ };
+
+ if (values is not { Length: > 0 })
+ {
+ return isf;
+ }
+
+ isf.Values = new List(values.Length);
+
+ foreach (var value in values)
+ {
+ isf.Values.Add(new CustomFieldValue { Info = value });
+ }
+
+ return isf;
+ }
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs
index 82522462..51ac6dc3 100644
--- a/src/redmine-net-api/Types/IssuePriority.cs
+++ b/src/redmine-net-api/Types/IssuePriority.cs
@@ -16,19 +16,19 @@ limitations under the License.
using System;
using System.Diagnostics;
-using System.Globalization;
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Json.Extensions;
namespace Redmine.Net.Api.Types
{
///
/// Availability 2.2
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.ISSUE_PRIORITY)]
public sealed class IssuePriority :
IdentifiableName
diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs
index f913221b..62ddfeea 100644
--- a/src/redmine-net-api/Types/IssueRelation.cs
+++ b/src/redmine-net-api/Types/IssueRelation.cs
@@ -16,7 +16,6 @@ limitations under the License.
using System;
using System.Diagnostics;
-using System.Globalization;
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
@@ -24,17 +23,18 @@ limitations under the License.
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
/// Availability 1.3
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.RELATION)]
- public sealed class IssueRelation :
- Identifiable
- ,ICloneable
+ public sealed class IssueRelation : Identifiable, ICloneable
{
#region Properties
///
@@ -115,8 +115,8 @@ public override void WriteXml(XmlWriter writer)
{
AssertValidIssueRelationType();
- writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToString(CultureInfo.InvariantCulture));
- writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInv());
+ writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToInvariantString());
+ writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToLowerName());
if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows)
{
@@ -137,7 +137,7 @@ public override void WriteJson(JsonWriter writer)
using (new JsonObject(writer, RedmineKeys.RELATION))
{
writer.WriteProperty(RedmineKeys.ISSUE_TO_ID, IssueToId);
- writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInv());
+ writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type.ToLowerName());
if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows)
{
diff --git a/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs
index fff16392..01d06292 100644
--- a/src/redmine-net-api/Types/IssueRelationType.cs
+++ b/src/redmine-net-api/Types/IssueRelationType.cs
@@ -68,13 +68,13 @@ public enum IssueRelationType
///
///
- [XmlEnum("copied_to")]
+ [XmlEnum(RedmineKeys.COPIED_TO)]
CopiedTo,
///
///
///
- [XmlEnum("copied_from")]
+ [XmlEnum(RedmineKeys.COPIED_FROM)]
CopiedFrom
}
}
\ No newline at end of file
diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs
index 25d2a30a..08c7553a 100644
--- a/src/redmine-net-api/Types/IssueStatus.cs
+++ b/src/redmine-net-api/Types/IssueStatus.cs
@@ -16,27 +16,40 @@ limitations under the License.
using System;
using System.Diagnostics;
-using System.Globalization;
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
/// Availability 1.3
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.ISSUE_STATUS)]
public sealed class IssueStatus : IdentifiableName, IEquatable, ICloneable
{
+ ///
+ ///
+ ///
public IssueStatus()
{
}
+ ///
+ ///
+ ///
+ ///
+ public IssueStatus(int id)
+ {
+ Id = id;
+ }
+
///
///
///
diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs
index bf016b1b..916967a1 100644
--- a/src/redmine-net-api/Types/Journal.cs
+++ b/src/redmine-net-api/Types/Journal.cs
@@ -23,13 +23,15 @@ limitations under the License.
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.JOURNAL)]
public sealed class Journal :
Identifiable
@@ -81,7 +83,7 @@ public sealed class Journal :
///
/// The details.
///
- public IList Details { get; internal set; }
+ public List Details { get; internal set; }
///
///
diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs
index 4b563614..d03fa95f 100644
--- a/src/redmine-net-api/Types/Membership.cs
+++ b/src/redmine-net-api/Types/Membership.cs
@@ -21,13 +21,15 @@ limitations under the License.
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
/// Only the roles can be updated, the project and the user of a membership are read-only.
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.MEMBERSHIP)]
public sealed class Membership : Identifiable
{
@@ -51,7 +53,7 @@ public sealed class Membership : Identifiable
/// Gets or sets the type.
///
/// The type.
- public IList Roles { get; internal set; }
+ public List Roles { get; internal set; }
#endregion
#region Implementation of IXmlSerialization
diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs
index 1de8fbfc..fcfab110 100644
--- a/src/redmine-net-api/Types/MembershipRole.cs
+++ b/src/redmine-net-api/Types/MembershipRole.cs
@@ -20,15 +20,18 @@ limitations under the License.
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
+using Redmine.Net.Api.Common;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.ROLE)]
public sealed class MembershipRole : IdentifiableName, IEquatable, IValue
{
@@ -100,7 +103,7 @@ public override void ReadJson(JsonReader reader)
///
public override void WriteJson(JsonWriter writer)
{
- writer.WriteProperty(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture));
+ writer.WriteProperty(RedmineKeys.ID, Id.ToInvariantString());
}
#endregion
@@ -172,7 +175,7 @@ public override int GetHashCode()
///
///
///
- public string Value => Id.ToString(CultureInfo.InvariantCulture);
+ public string Value => Id.ToInvariantString();
#endregion
///
diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs
index 806123b4..570c5d67 100644
--- a/src/redmine-net-api/Types/MyAccount.cs
+++ b/src/redmine-net-api/Types/MyAccount.cs
@@ -17,13 +17,15 @@ limitations under the License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
@@ -31,7 +33,7 @@ namespace Redmine.Net.Api.Types
///
///
/// Availability 4.1
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.USER)]
public sealed class MyAccount : Identifiable
{
diff --git a/src/redmine-net-api/Types/MyAccountCustomField.cs b/src/redmine-net-api/Types/MyAccountCustomField.cs
index 55372989..3015296a 100644
--- a/src/redmine-net-api/Types/MyAccountCustomField.cs
+++ b/src/redmine-net-api/Types/MyAccountCustomField.cs
@@ -21,13 +21,14 @@ limitations under the License.
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
+using Redmine.Net.Api.Serialization.Json.Extensions;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.CUSTOM_FIELD)]
public sealed class MyAccountCustomField : IdentifiableName, IEquatable
{
diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs
index c989c6f2..6e830f72 100644
--- a/src/redmine-net-api/Types/News.cs
+++ b/src/redmine-net-api/Types/News.cs
@@ -17,20 +17,22 @@ limitations under the License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
/// Availability 1.1
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.NEWS)]
public sealed class News : Identifiable
{
diff --git a/src/redmine-net-api/Types/NewsComment.cs b/src/redmine-net-api/Types/NewsComment.cs
index 86eca5ed..b80953e4 100644
--- a/src/redmine-net-api/Types/NewsComment.cs
+++ b/src/redmine-net-api/Types/NewsComment.cs
@@ -27,7 +27,7 @@ namespace Redmine.Net.Api.Types
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.COMMENT)]
public sealed class NewsComment: Identifiable
{
diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs
index 8d300411..7612ecef 100644
--- a/src/redmine-net-api/Types/Permission.cs
+++ b/src/redmine-net-api/Types/Permission.cs
@@ -22,13 +22,14 @@ limitations under the License.
using Newtonsoft.Json;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
namespace Redmine.Net.Api.Types
{
///
///
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.PERMISSION)]
#pragma warning disable CA1711
public sealed class Permission : IXmlSerializable, IJsonSerializable, IEquatable
diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs
index 798ab732..5ff1f11f 100644
--- a/src/redmine-net-api/Types/Project.cs
+++ b/src/redmine-net-api/Types/Project.cs
@@ -17,20 +17,23 @@ limitations under the License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
using System.Xml;
using System.Xml.Serialization;
using Newtonsoft.Json;
+using Redmine.Net.Api.Common;
using Redmine.Net.Api.Extensions;
using Redmine.Net.Api.Internals;
using Redmine.Net.Api.Serialization;
+using Redmine.Net.Api.Serialization.Json;
+using Redmine.Net.Api.Serialization.Json.Extensions;
+using Redmine.Net.Api.Serialization.Xml.Extensions;
namespace Redmine.Net.Api.Types
{
///
/// Availability 1.0
///
- [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
+ [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")]
[XmlRoot(RedmineKeys.PROJECT)]
public sealed class Project : IdentifiableName, IEquatable
{
@@ -105,7 +108,7 @@ public sealed class Project : IdentifiableName, IEquatable
/// The trackers.
///
///