diff --git a/src/Extensions/Cloudflare.Dns/CustomNameserversExtensions.cs b/src/Extensions/Cloudflare.Dns/CustomNameserversExtensions.cs
new file mode 100644
index 0000000..d4e5be7
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/CustomNameserversExtensions.cs
@@ -0,0 +1,61 @@
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare.Dns.Internals;
+
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Extensions for Account Custom Nameservers.
+ ///
+ public static class CustomNameserversExtensions
+ {
+ ///
+ /// Add Account Custom Nameserver.
+ ///
+ /// The instance.
+ /// The request.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task> AddCustomNameserver(this ICloudflareClient client, AddCustomNameserverRequest request, CancellationToken cancellationToken = default)
+ {
+ request.AccountId.ValidateCloudflareId();
+
+ var req = new InternalAddCustomNameserverRequest
+ {
+ NameserverName = request.NameserverName,
+ NameserverSet = request.NameserverSet
+ };
+
+ return client.PostAsync($"/accounts/{request.AccountId}/custom_ns", req, null, cancellationToken);
+ }
+
+ ///
+ /// Delete Account Custom Nameserver.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// The nameserver identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task>> DeleteCustomNameserver(this ICloudflareClient client, string accountId, string nameserverId, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+
+ if (string.IsNullOrWhiteSpace(nameserverId))
+ throw new ArgumentNullException(nameof(nameserverId));
+
+ return client.DeleteAsync>($"/accounts/{accountId}/custom_ns/{nameserverId}", null, cancellationToken);
+ }
+
+ ///
+ /// List an account's custom nameservers.
+ ///
+ /// The instance.
+ /// The account identifier.
+ /// A cancellation token used to propagate notification that this operation should be canceled.
+ public static Task>> ListCustomNameserver(this ICloudflareClient client, string accountId, CancellationToken cancellationToken = default)
+ {
+ accountId.ValidateCloudflareId();
+
+ return client.GetAsync>($"/accounts/{accountId}/custom_ns", null, cancellationToken);
+ }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Internals/InternalAddCustomNameserverRequest.cs b/src/Extensions/Cloudflare.Dns/Internals/InternalAddCustomNameserverRequest.cs
new file mode 100644
index 0000000..16840e4
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Internals/InternalAddCustomNameserverRequest.cs
@@ -0,0 +1,11 @@
+namespace AMWD.Net.Api.Cloudflare.Dns.Internals
+{
+ internal class InternalAddCustomNameserverRequest
+ {
+ [JsonProperty("ns_name")]
+ public string? NameserverName { get; set; }
+
+ [JsonProperty("ns_set")]
+ public int? NameserverSet { get; set; }
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/Models/CustomNameserver.cs b/src/Extensions/Cloudflare.Dns/Models/CustomNameserver.cs
new file mode 100644
index 0000000..25201bc
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Models/CustomNameserver.cs
@@ -0,0 +1,121 @@
+using System.Runtime.Serialization;
+using Newtonsoft.Json.Converters;
+
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// A Cloudflare custom nameserver.
+ /// Source
+ ///
+ public class CustomNameserver
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The FQDN of the name server.
+ public CustomNameserver(string nameserverName)
+ {
+ NameserverName = nameserverName;
+ }
+
+ ///
+ /// A and AAAA records associated with the nameserver.
+ ///
+ [JsonProperty("dns_records")]
+ public IReadOnlyCollection? DnsRecords { get; set; }
+
+ ///
+ /// The full qualified domain name (FQDN) of the name server.
+ ///
+ [JsonProperty("ns_name")]
+ public string NameserverName { get; set; }
+
+ ///
+ /// Verification status of the nameserver.
+ ///
+ [Obsolete]
+ [JsonProperty("status")]
+ public CustomNameserverStatus? Status { get; set; }
+
+ ///
+ /// Identifier.
+ ///
+ [JsonProperty("zone_tag")]
+ public string? ZoneTag { get; set; }
+
+ ///
+ /// The number of the set that this name server belongs to.
+ ///
+ ///
+ /// 1 <= X <= 5
+ ///
+ /// Default: 1
+ ///
+ [JsonProperty("ns_set")]
+ public int? NameserverSet { get; set; }
+ }
+
+ ///
+ /// Records associated with the nameserver.
+ ///
+ public class CustomNameserverDnsRecord
+ {
+ ///
+ /// DNS record type.
+ ///
+ [JsonProperty("type")]
+ public CustomNameserverDnsRecordType? Type { get; set; }
+
+ ///
+ /// DNS record contents (an IPv4 or IPv6 address).
+ ///
+ [JsonProperty("value")]
+ public string? Value { get; set; }
+ }
+
+ ///
+ /// Record types.
+ /// Source
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum CustomNameserverDnsRecordType
+ {
+ ///
+ /// IPv4 record.
+ ///
+ [EnumMember(Value = "A")]
+ A = 1,
+
+ ///
+ /// IPv6 record.
+ ///
+ [EnumMember(Value = "AAAA")]
+ AAAA = 2
+ }
+
+ ///
+ /// Custom nameserver states.
+ /// Source
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum CustomNameserverStatus
+ {
+ ///
+ /// The nameserver has been moved.
+ ///
+ [EnumMember(Value = "moved")]
+ Moved = 1,
+
+ ///
+ /// The nameserver is pending verification.
+ ///
+ [EnumMember(Value = "pending")]
+ Pending = 2,
+
+ ///
+ /// The nameserver has been verified.
+ ///
+ [EnumMember(Value = "verified")]
+ Verified = 3
+ }
+}
diff --git a/src/Extensions/Cloudflare.Dns/README.md b/src/Extensions/Cloudflare.Dns/README.md
index 1d7a63a..7f9f404 100644
--- a/src/Extensions/Cloudflare.Dns/README.md
+++ b/src/Extensions/Cloudflare.Dns/README.md
@@ -4,6 +4,13 @@ This package contains the feature set of the _DNS_ section of the Cloudflare API
## Implemented Methods
+### [Account Custom Nameservers]
+
+- [Add Account Custom Nameserver](https://developers.cloudflare.com/api/resources/custom_nameservers/methods/create/)
+- [Delete Account Custom Nameserver](https://developers.cloudflare.com/api/resources/custom_nameservers/methods/delete/)
+- [List Account Custom Nameservers](https://developers.cloudflare.com/api/resources/custom_nameservers/methods/get/)
+
+
---
@@ -12,3 +19,5 @@ Published under MIT License (see [choose a license])
[choose a license]: https://choosealicense.com/licenses/mit/
+
+[Account Custom Nameservers]: https://developers.cloudflare.com/api/resources/custom_nameservers/
diff --git a/src/Extensions/Cloudflare.Dns/Requests/AddCustomNameserverRequest.cs b/src/Extensions/Cloudflare.Dns/Requests/AddCustomNameserverRequest.cs
new file mode 100644
index 0000000..c01b0b9
--- /dev/null
+++ b/src/Extensions/Cloudflare.Dns/Requests/AddCustomNameserverRequest.cs
@@ -0,0 +1,34 @@
+namespace AMWD.Net.Api.Cloudflare.Dns
+{
+ ///
+ /// Represents a request to add a custom nameserver.
+ ///
+ public class AddCustomNameserverRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The account identifier.
+ /// The FQDN of the name server.
+ public AddCustomNameserverRequest(string accountId, string nameserverName)
+ {
+ AccountId = accountId;
+ NameserverName = nameserverName;
+ }
+
+ ///
+ /// The account identifier.
+ ///
+ public string AccountId { get; set; }
+
+ ///
+ /// The FQDN of the name server.
+ ///
+ public string NameserverName { get; set; }
+
+ ///
+ /// The number of the set that this name server belongs to.
+ ///
+ public int? NameserverSet { get; set; }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/AddCustomNameserverTest.cs b/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/AddCustomNameserverTest.cs
new file mode 100644
index 0000000..73e726f
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/AddCustomNameserverTest.cs
@@ -0,0 +1,84 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Dns;
+using AMWD.Net.Api.Cloudflare.Dns.Internals;
+using Moq;
+
+namespace Cloudflare.Dns.Tests.CustomNameserversExtensions
+{
+ [TestClass]
+ public class AddCustomNameserverTest
+ {
+ private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
+
+ private const string Nameserver = "ns1.example.com";
+
+ private Mock _clientMock;
+
+ private CloudflareResponse _response;
+
+ private List<(string RequestPath, InternalAddCustomNameserverRequest Request)> _callbacks;
+
+ private AddCustomNameserverRequest _request;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(1000, "Error 1")
+ ],
+ Result = new CustomNameserver(Nameserver)
+ };
+
+ _request = new AddCustomNameserverRequest(AccountId, Nameserver);
+ }
+
+ [TestMethod]
+ public async Task ShouldAddCustomNameserver()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.AddCustomNameserver(_request);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.AreEqual(1, _callbacks.Count);
+
+ var callback = _callbacks.First();
+ Assert.AreEqual($"/accounts/{AccountId}/custom_ns", callback.RequestPath);
+ Assert.IsNotNull(callback.Request);
+
+ Assert.AreEqual(_request.NameserverName, callback.Request.NameserverName);
+ Assert.IsNull(callback.Request.NameserverSet);
+
+ _clientMock.Verify(m => m.PostAsync($"/accounts/{AccountId}/custom_ns", It.IsAny(), null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.PostAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, request, _, _) => _callbacks.Add((requestPath, request)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/DeleteCustomNameserverTest.cs b/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/DeleteCustomNameserverTest.cs
new file mode 100644
index 0000000..d91e50f
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/DeleteCustomNameserverTest.cs
@@ -0,0 +1,92 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Dns;
+using Moq;
+
+namespace Cloudflare.Dns.Tests.CustomNameserversExtensions
+{
+ [TestClass]
+ public class DeleteCustomNameserverTest
+ {
+ private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
+
+ private const string Nameserver = "ns1.example.com";
+
+ private Mock _clientMock;
+
+ private CloudflareResponse> _response;
+
+ private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse>
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(1000, "Error 1")
+ ],
+ Result = []
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldDeleteCustomNameserver()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.DeleteCustomNameserver(AccountId, Nameserver);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.AreEqual(1, _callbacks.Count);
+
+ var callback = _callbacks.First();
+ Assert.AreEqual($"/accounts/{AccountId}/custom_ns/{Nameserver}", callback.RequestPath);
+ Assert.IsNull(callback.QueryFilter);
+
+ _clientMock.Verify(m => m.DeleteAsync>($"/accounts/{AccountId}/custom_ns/{Nameserver}", null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ [DataTestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public async Task ShouldDeleteCustomNameserver(string nameserver)
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.DeleteCustomNameserver(AccountId, nameserver);
+
+ // Assert - ArgumentNullException
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.DeleteAsync>(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}
diff --git a/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/ListCustomNameserverTest.cs b/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/ListCustomNameserverTest.cs
new file mode 100644
index 0000000..4bb2372
--- /dev/null
+++ b/test/Extensions/Cloudflare.Dns.Tests/CustomNameserversExtensions/ListCustomNameserverTest.cs
@@ -0,0 +1,76 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using AMWD.Net.Api.Cloudflare;
+using AMWD.Net.Api.Cloudflare.Dns;
+using Moq;
+
+namespace Cloudflare.Dns.Tests.CustomNameserversExtensions
+{
+ [TestClass]
+ public class ListCustomNameserverTest
+ {
+ private const string AccountId = "023e105f4ecef8ad9ca31a8372d0c353";
+
+ private const string Nameserver = "ns1.example.com";
+
+ private Mock _clientMock;
+
+ private CloudflareResponse> _response;
+
+ private List<(string RequestPath, IQueryParameterFilter QueryFilter)> _callbacks;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _callbacks = [];
+
+ _response = new CloudflareResponse>
+ {
+ Success = true,
+ Messages = [
+ new ResponseInfo(1000, "Message 1")
+ ],
+ Errors = [
+ new ResponseInfo(1000, "Error 1")
+ ],
+ Result = []
+ };
+ }
+
+ [TestMethod]
+ public async Task ShouldListCustomNameserver()
+ {
+ // Arrange
+ var client = GetClient();
+
+ // Act
+ var response = await client.ListCustomNameserver(AccountId);
+
+ // Assert
+ Assert.IsNotNull(response);
+ Assert.IsTrue(response.Success);
+ Assert.AreEqual(_response.Result, response.Result);
+
+ Assert.AreEqual(1, _callbacks.Count);
+
+ var callback = _callbacks.First();
+ Assert.AreEqual($"/accounts/{AccountId}/custom_ns", callback.RequestPath);
+ Assert.IsNull(callback.QueryFilter);
+
+ _clientMock.Verify(m => m.GetAsync>($"/accounts/{AccountId}/custom_ns", null, It.IsAny()), Times.Once);
+ _clientMock.VerifyNoOtherCalls();
+ }
+
+ private ICloudflareClient GetClient()
+ {
+ _clientMock = new Mock();
+ _clientMock
+ .Setup(m => m.GetAsync>(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((requestPath, queryFilter, _) => _callbacks.Add((requestPath, queryFilter)))
+ .ReturnsAsync(() => _response);
+
+ return _clientMock.Object;
+ }
+ }
+}