diff --git a/src/clients/dotnet/Bitip.Client.Tests/Bitip.Client.Tests.csproj b/src/clients/dotnet/Bitip.Client.Tests/Bitip.Client.Tests.csproj
index 0e0aa05..c720486 100644
--- a/src/clients/dotnet/Bitip.Client.Tests/Bitip.Client.Tests.csproj
+++ b/src/clients/dotnet/Bitip.Client.Tests/Bitip.Client.Tests.csproj
@@ -3,6 +3,27 @@
net9.0
enable
+ false
+ true
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
diff --git a/src/clients/dotnet/Bitip.Client.Tests/Class1.cs b/src/clients/dotnet/Bitip.Client.Tests/Class1.cs
deleted file mode 100644
index 2c0fa9a..0000000
--- a/src/clients/dotnet/Bitip.Client.Tests/Class1.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright (c) 2025 Tudor Stanciu
-
-namespace Bitip.Client.Tests
-{
- public class Class1
- {
-
- }
-}
diff --git a/src/clients/dotnet/Bitip.Client.Tests/GetDetailedIpLocationTests.cs b/src/clients/dotnet/Bitip.Client.Tests/GetDetailedIpLocationTests.cs
new file mode 100644
index 0000000..9e0c312
--- /dev/null
+++ b/src/clients/dotnet/Bitip.Client.Tests/GetDetailedIpLocationTests.cs
@@ -0,0 +1,250 @@
+// Copyright (c) 2025 Tudor Stanciu
+
+using Bitip.Client.Models;
+using Bitip.Client.Tests.Helpers;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Bitip.Client.Tests
+{
+ ///
+ /// Unit tests for IBitipClient.GetDetailedIpLocation method (detailed lookup).
+ ///
+ public class GetDetailedIpLocationTests
+ {
+ [Fact]
+ public async Task GetDetailedIpLocation_WithValidIp_ReturnsDetailedLocation()
+ {
+ // Arrange
+ var expectedLocation = new DetailedIpLocation
+ {
+ Ip = "8.8.8.8",
+ Location = new GeoIpLocation
+ {
+ Country = new CountryInfo
+ {
+ IsoCode = "US",
+ Names = new Dictionary
+ {
+ { "en", "United States" },
+ { "de", "Vereinigte Staaten" }
+ },
+ IsInEuropeanUnion = false
+ },
+ City = new CityInfo
+ {
+ Names = new Dictionary
+ {
+ { "en", "Mountain View" }
+ }
+ },
+ Subdivisions = new List
+ {
+ new Subdivision
+ {
+ IsoCode = "CA",
+ Names = new Dictionary
+ {
+ { "en", "California" }
+ }
+ }
+ },
+ Location = new LocationInfo
+ {
+ Latitude = 37.4056,
+ Longitude = -122.0775,
+ TimeZone = "America/Los_Angeles"
+ },
+ Postal = new PostalInfo
+ {
+ Code = "94043"
+ },
+ Continent = new ContinentInfo
+ {
+ Code = "NA",
+ Names = new Dictionary
+ {
+ { "en", "North America" }
+ }
+ },
+ Traits = new TraitsInfo
+ {
+ IsAnonymousProxy = false,
+ IsSatelliteProvider = false
+ }
+ },
+ Asn = new AsnInfo
+ {
+ AutonomousSystemNumber = 15169,
+ AutonomousSystemOrganization = "Google LLC",
+ IpAddress = "8.8.8.8",
+ Network = "8.8.8.0/24"
+ }
+ };
+
+ var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedLocation, "lookup/detailed?ip=8.8.8.8");
+
+ // Act
+ var result = await client.GetDetailedIpLocation("8.8.8.8");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("8.8.8.8", result.Ip);
+ Assert.NotNull(result.Location);
+ Assert.NotNull(result.Location.Country);
+ Assert.Equal("US", result.Location.Country.IsoCode);
+ Assert.Equal("United States", result.Location.Country.Names?["en"]);
+ Assert.Equal("Mountain View", result.Location.City?.Names?["en"]);
+ Assert.Equal(37.4056, result.Location.Location?.Latitude);
+ Assert.Equal(-122.0775, result.Location.Location?.Longitude);
+ Assert.NotNull(result.Asn);
+ Assert.Equal((long)15169, result.Asn.AutonomousSystemNumber);
+ Assert.Equal("Google LLC", result.Asn.AutonomousSystemOrganization);
+ }
+
+ [Fact]
+ public async Task GetDetailedIpLocation_WithIpV6_ReturnsDetailedLocation()
+ {
+ // Arrange
+ var expectedLocation = new DetailedIpLocation
+ {
+ Ip = "2001:4860:4860::8888",
+ Location = new GeoIpLocation
+ {
+ Country = new CountryInfo
+ {
+ IsoCode = "US",
+ Names = new Dictionary { { "en", "United States" } }
+ },
+ City = new CityInfo
+ {
+ Names = new Dictionary { { "en", "Mountain View" } }
+ }
+ },
+ Asn = new AsnInfo
+ {
+ AutonomousSystemNumber = 15169,
+ AutonomousSystemOrganization = "Google LLC"
+ }
+ };
+
+ var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedLocation, "lookup/detailed");
+
+ // Act
+ var result = await client.GetDetailedIpLocation("2001:4860:4860::8888");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("2001:4860:4860::8888", result.Ip);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public async Task GetDetailedIpLocation_WithNullOrEmptyIp_ThrowsArgumentException(string? ip)
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateMockedClient(mock => { });
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ await client.GetDetailedIpLocation(ip!));
+ }
+
+ [Fact]
+ public async Task GetDetailedIpLocation_WithInvalidIpFormat_ThrowsArgumentException()
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateMockedClient(mock => { });
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ await client.GetDetailedIpLocation("not-an-ip"));
+ }
+
+ [Fact]
+ public async Task GetDetailedIpLocation_WithNotFoundIp_ThrowsHttpRequestException()
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateClientWithError(
+ HttpStatusCode.NotFound,
+ "Not Found",
+ "IP address not found in database",
+ "1.2.3.4");
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(async () =>
+ await client.GetDetailedIpLocation("1.2.3.4"));
+
+ Assert.Contains("Not Found", exception.Message);
+ }
+
+ [Fact]
+ public async Task GetDetailedIpLocation_WithCancellation_ThrowsTaskCanceledException()
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateCancelledClient();
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ await client.GetDetailedIpLocation("8.8.8.8", cts.Token));
+ }
+
+ [Fact]
+ public async Task GetDetailedIpLocation_WithServiceUnavailable_ThrowsHttpRequestException()
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateClientWithError(
+ HttpStatusCode.ServiceUnavailable,
+ "Service Unavailable",
+ "Under maintenance, try again later");
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(async () =>
+ await client.GetDetailedIpLocation("8.8.8.8"));
+
+ Assert.Contains("Service Unavailable", exception.Message);
+ }
+
+ [Fact]
+ public async Task GetDetailedIpLocation_VerifyAsnInformation_ContainsExpectedFields()
+ {
+ // Arrange
+ var expectedLocation = new DetailedIpLocation
+ {
+ Ip = "1.1.1.1",
+ Location = new GeoIpLocation
+ {
+ Country = new CountryInfo { IsoCode = "AU" }
+ },
+ Asn = new AsnInfo
+ {
+ AutonomousSystemNumber = 13335,
+ AutonomousSystemOrganization = "Cloudflare, Inc.",
+ IpAddress = "1.1.1.1",
+ Network = "1.1.1.0/24"
+ }
+ };
+
+ var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedLocation, "lookup/detailed");
+
+ // Act
+ var result = await client.GetDetailedIpLocation("1.1.1.1");
+
+ // Assert
+ Assert.NotNull(result.Asn);
+ Assert.Equal((long)13335, result.Asn.AutonomousSystemNumber);
+ Assert.Equal("Cloudflare, Inc.", result.Asn.AutonomousSystemOrganization);
+ Assert.Equal("1.1.1.1", result.Asn.IpAddress);
+ Assert.Equal("1.1.1.0/24", result.Asn.Network);
+ }
+ }
+}
diff --git a/src/clients/dotnet/Bitip.Client.Tests/GetHealthTests.cs b/src/clients/dotnet/Bitip.Client.Tests/GetHealthTests.cs
new file mode 100644
index 0000000..fe7026c
--- /dev/null
+++ b/src/clients/dotnet/Bitip.Client.Tests/GetHealthTests.cs
@@ -0,0 +1,70 @@
+// Copyright (c) 2025 Tudor Stanciu
+
+using Bitip.Client.Models;
+using Bitip.Client.Tests.Helpers;
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Bitip.Client.Tests
+{
+ ///
+ /// Unit tests for IBitipClient.GetHealth method.
+ ///
+ public class GetHealthTests
+ {
+ [Fact]
+ public async Task GetHealth_WithMockedResponse_ReturnsHealthInfo()
+ {
+ // Arrange
+ var expectedHealth = new HealthInfo
+ {
+ Status = "healthy",
+ Service = "Bitip GeoIP Service",
+ Timestamp = DateTime.UtcNow,
+ Error = null
+ };
+
+ var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedHealth, "health");
+
+ // Act
+ var result = await client.GetHealth();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("healthy", result.Status);
+ Assert.Equal("Bitip GeoIP Service", result.Service);
+ Assert.Null(result.Error);
+ }
+
+ [Fact]
+ public async Task GetHealth_WithCancellation_ThrowsTaskCanceledException()
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateCancelledClient();
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ await client.GetHealth(cts.Token));
+ }
+
+ [Fact]
+ public async Task GetHealth_WithServerError_ThrowsHttpRequestException()
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateClientWithError(
+ HttpStatusCode.ServiceUnavailable,
+ "Service Unavailable",
+ "Under maintenance");
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ await client.GetHealth());
+ }
+ }
+}
diff --git a/src/clients/dotnet/Bitip.Client.Tests/GetIpLocationTests.cs b/src/clients/dotnet/Bitip.Client.Tests/GetIpLocationTests.cs
new file mode 100644
index 0000000..db1be16
--- /dev/null
+++ b/src/clients/dotnet/Bitip.Client.Tests/GetIpLocationTests.cs
@@ -0,0 +1,157 @@
+// Copyright (c) 2025 Tudor Stanciu
+
+using Bitip.Client.Models;
+using Bitip.Client.Tests.Helpers;
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Bitip.Client.Tests
+{
+ ///
+ /// Unit tests for IBitipClient.GetIpLocation method (simple lookup).
+ ///
+ public class GetIpLocationTests
+ {
+ [Fact]
+ public async Task GetIpLocation_WithValidIpV4_ReturnsLocation()
+ {
+ // Arrange
+ var expectedLocation = new IpLocation
+ {
+ Ip = "8.8.8.8",
+ Country = "United States",
+ CountryCode = "US",
+ IsInEuropeanUnion = false,
+ Region = "California",
+ RegionCode = "CA",
+ City = "Mountain View",
+ Latitude = 37.4056,
+ Longitude = -122.0775,
+ Timezone = "America/Los_Angeles",
+ PostalCode = "94043",
+ ContinentCode = "NA",
+ ContinentName = "North America",
+ Organization = "Google LLC"
+ };
+
+ var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedLocation, "lookup?ip=8.8.8.8");
+
+ // Act
+ var result = await client.GetIpLocation("8.8.8.8");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("8.8.8.8", result.Ip);
+ Assert.Equal("United States", result.Country);
+ Assert.Equal("US", result.CountryCode);
+ Assert.False(result.IsInEuropeanUnion);
+ Assert.Equal("California", result.Region);
+ Assert.Equal("Mountain View", result.City);
+ Assert.Equal(37.4056, result.Latitude);
+ Assert.Equal(-122.0775, result.Longitude);
+ }
+
+ [Fact]
+ public async Task GetIpLocation_WithValidIpV6_ReturnsLocation()
+ {
+ // Arrange
+ var expectedLocation = new IpLocation
+ {
+ Ip = "2001:4860:4860::8888",
+ Country = "United States",
+ CountryCode = "US",
+ IsInEuropeanUnion = false,
+ Region = "California",
+ City = "Mountain View",
+ Latitude = 37.4056,
+ Longitude = -122.0775,
+ Timezone = "America/Los_Angeles"
+ };
+
+ var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedLocation, "lookup?ip=2001:4860:4860::8888");
+
+ // Act
+ var result = await client.GetIpLocation("2001:4860:4860::8888");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("2001:4860:4860::8888", result.Ip);
+ Assert.Equal("United States", result.Country);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public async Task GetIpLocation_WithNullOrEmptyIp_ThrowsArgumentException(string? ip)
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateMockedClient(mock => { });
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ await client.GetIpLocation(ip!));
+ }
+
+ [Fact]
+ public async Task GetIpLocation_WithInvalidIpFormat_ThrowsArgumentException()
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateMockedClient(mock => { });
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ await client.GetIpLocation("invalid-ip-address"));
+ }
+
+ [Fact]
+ public async Task GetIpLocation_WithNotFoundIp_ThrowsHttpRequestException()
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateClientWithError(
+ HttpStatusCode.NotFound,
+ "Not Found",
+ "IP address not found in database",
+ "1.2.3.4");
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(async () =>
+ await client.GetIpLocation("1.2.3.4"));
+
+ Assert.Contains("Not Found", exception.Message);
+ }
+
+ [Fact]
+ public async Task GetIpLocation_WithCancellation_ThrowsTaskCanceledException()
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateCancelledClient();
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ await client.GetIpLocation("8.8.8.8", cts.Token));
+ }
+
+ [Fact]
+ public async Task GetIpLocation_WithRateLimit_ThrowsHttpRequestException()
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateClientWithError(
+ (HttpStatusCode)429,
+ "Too Many Requests",
+ "Rate limit exceeded");
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(async () =>
+ await client.GetIpLocation("8.8.8.8"));
+
+ Assert.Contains("Too Many Requests", exception.Message);
+ }
+ }
+}
diff --git a/src/clients/dotnet/Bitip.Client.Tests/GetVersionTests.cs b/src/clients/dotnet/Bitip.Client.Tests/GetVersionTests.cs
new file mode 100644
index 0000000..41839fd
--- /dev/null
+++ b/src/clients/dotnet/Bitip.Client.Tests/GetVersionTests.cs
@@ -0,0 +1,71 @@
+// Copyright (c) 2025 Tudor Stanciu
+
+using Bitip.Client.Models;
+using Bitip.Client.Tests.Helpers;
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Bitip.Client.Tests
+{
+ ///
+ /// Unit tests for IBitipClient.GetVersion method.
+ ///
+ public class GetVersionTests
+ {
+ [Fact]
+ public async Task GetVersion_WithMockedResponse_ReturnsVersionInfo()
+ {
+ // Arrange
+ var expectedVersion = new VersionInfo
+ {
+ Version = "1.0.0",
+ CommitHash = "abc123def456",
+ BuildDate = new DateTime(2025, 10, 8)
+ };
+
+ var client = BitipClientTestFixture.CreateMockedClientWithResponse(expectedVersion, "version");
+
+ // Act
+ var result = await client.GetVersion();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("1.0.0", result.Version);
+ Assert.Equal("abc123def456", result.CommitHash);
+ Assert.Equal(new DateTime(2025, 10, 8), result.BuildDate);
+ }
+
+ [Fact]
+ public async Task GetVersion_WithCancellation_ThrowsTaskCanceledException()
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateCancelledClient();
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ await client.GetVersion(cts.Token));
+ }
+
+ [Fact]
+ public async Task GetVersion_WithUnauthorized_ThrowsHttpRequestException()
+ {
+ // Arrange
+ var client = BitipClientTestFixture.CreateClientWithError(
+ HttpStatusCode.Unauthorized,
+ "Unauthorized",
+ "Invalid API key");
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(async () =>
+ await client.GetVersion());
+
+ Assert.Contains("Unauthorized", exception.Message);
+ }
+ }
+}
diff --git a/src/clients/dotnet/Bitip.Client.Tests/Helpers/BitipClientTestFixture.cs b/src/clients/dotnet/Bitip.Client.Tests/Helpers/BitipClientTestFixture.cs
new file mode 100644
index 0000000..071a31e
--- /dev/null
+++ b/src/clients/dotnet/Bitip.Client.Tests/Helpers/BitipClientTestFixture.cs
@@ -0,0 +1,130 @@
+// Copyright (c) 2025 Tudor Stanciu
+
+using Bitip.Client.Models;
+using Bitip.Client.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Moq.Protected;
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Bitip.Client.Tests.Helpers
+{
+ ///
+ /// Helper class for creating mocked BitipClient instances in tests.
+ ///
+ public static class BitipClientTestFixture
+ {
+ ///
+ /// Creates a mocked BitipClient with a custom HttpMessageHandler setup.
+ ///
+ public static IBitipClient CreateMockedClient(Action> setupHandler)
+ {
+ var mockHandler = new Mock();
+ setupHandler(mockHandler);
+
+ var httpClient = new HttpClient(mockHandler.Object)
+ {
+ BaseAddress = new Uri(TestConfiguration.MockApiBaseUrl)
+ };
+
+ return new BitipClient(
+ httpClient,
+ Microsoft.Extensions.Options.Options.Create(new BitipOptions
+ {
+ BaseUrl = TestConfiguration.MockApiBaseUrl,
+ ApiKey = TestConfiguration.MockApiKey
+ }));
+ }
+
+ ///
+ /// Creates a mocked client that returns a successful response with the provided data.
+ ///
+ public static IBitipClient CreateMockedClientWithResponse(T responseData, string urlContains = "")
+ {
+ return CreateMockedClient(mockHandler =>
+ {
+ mockHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ string.IsNullOrEmpty(urlContains)
+ ? ItExpr.IsAny()
+ : ItExpr.Is(req =>
+ req.Method == HttpMethod.Get &&
+ req.RequestUri!.ToString().Contains(urlContains)),
+ ItExpr.IsAny())
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(JsonSerializer.Serialize(responseData))
+ });
+ });
+ }
+
+ ///
+ /// Creates a mocked client that throws TaskCanceledException.
+ ///
+ public static IBitipClient CreateCancelledClient()
+ {
+ return CreateMockedClient(mockHandler =>
+ {
+ mockHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ThrowsAsync(new TaskCanceledException());
+ });
+ }
+
+ ///
+ /// Creates a mocked client that returns an HTTP error with specified status code.
+ ///
+ public static IBitipClient CreateClientWithError(HttpStatusCode statusCode, string error, string message, string? ip = null)
+ {
+ return CreateMockedClient(mockHandler =>
+ {
+ mockHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = statusCode,
+ Content = new StringContent(JsonSerializer.Serialize(new ErrorResponse
+ {
+ Error = error,
+ Message = message,
+ Ip = ip
+ }))
+ });
+ });
+ }
+
+ ///
+ /// Creates a real service provider for integration tests (if configured).
+ /// Returns null if TestConfiguration.UseRealApi is false.
+ ///
+#pragma warning disable CS0162 // Unreachable code detected (by design - UseRealApi is const)
+ public static IServiceProvider? CreateRealServiceProvider()
+ {
+ if (!TestConfiguration.UseRealApi)
+ {
+ return null;
+ }
+
+ var services = new ServiceCollection();
+ services.UseBitipClient(TestConfiguration.RealApiBaseUrl, TestConfiguration.RealApiKey);
+ return services.BuildServiceProvider();
+ }
+#pragma warning restore CS0162
+ }
+}
diff --git a/src/clients/dotnet/Bitip.Client.Tests/Integration/RealApiIntegrationTests.cs b/src/clients/dotnet/Bitip.Client.Tests/Integration/RealApiIntegrationTests.cs
new file mode 100644
index 0000000..4f6cefd
--- /dev/null
+++ b/src/clients/dotnet/Bitip.Client.Tests/Integration/RealApiIntegrationTests.cs
@@ -0,0 +1,106 @@
+// Copyright (c) 2025 Tudor Stanciu
+
+using Bitip.Client.Services;
+using Bitip.Client.Tests.Helpers;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Bitip.Client.Tests.Integration
+{
+ ///
+ /// Integration tests that make real API calls to a live Bitip instance.
+ /// These tests are DISABLED by default (TestConfiguration.UseRealApi = false).
+ ///
+ public class RealApiIntegrationTests
+ {
+ private readonly IServiceProvider? _realServiceProvider;
+
+ public RealApiIntegrationTests()
+ {
+ _realServiceProvider = BitipClientTestFixture.CreateRealServiceProvider();
+ }
+
+ [Fact]
+ public async Task GetHealth_ReturnsHealthInfo()
+ {
+ // Skip if not configured for real API
+ if (!TestConfiguration.UseRealApi || _realServiceProvider == null)
+ return;
+
+ // Arrange
+ var client = _realServiceProvider.GetRequiredService();
+
+ // Act
+ var result = await client.GetHealth();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.NotNull(result.Status);
+ Assert.NotNull(result.Service);
+ Assert.True(result.Timestamp > DateTime.MinValue);
+ }
+
+ [Fact]
+ public async Task GetVersion_ReturnsVersionInfo()
+ {
+ // Skip if not configured for real API
+ if (!TestConfiguration.UseRealApi || _realServiceProvider == null)
+ return;
+
+ // Arrange
+ var client = _realServiceProvider.GetRequiredService();
+
+ // Act
+ var result = await client.GetVersion();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.NotNull(result.Version);
+ Assert.NotNull(result.CommitHash);
+ Assert.True(result.BuildDate > DateTime.MinValue);
+ }
+
+ [Fact]
+ public async Task GetIpLocation_ReturnsLocation()
+ {
+ // Skip if not configured for real API
+ if (!TestConfiguration.UseRealApi || _realServiceProvider == null)
+ return;
+
+ // Arrange
+ var client = _realServiceProvider.GetRequiredService();
+
+ // Act
+ var result = await client.GetIpLocation(TestConfiguration.ValidIpV4);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(TestConfiguration.ValidIpV4, result.Ip);
+ Assert.NotNull(result.Country);
+ Assert.NotNull(result.CountryCode);
+ Assert.NotNull(result.City);
+ }
+
+ [Fact]
+ public async Task GetDetailedIpLocation_ReturnsDetailedLocation()
+ {
+ // Skip if not configured for real API
+ if (!TestConfiguration.UseRealApi || _realServiceProvider == null)
+ return;
+
+ // Arrange
+ var client = _realServiceProvider.GetRequiredService();
+
+ // Act
+ var result = await client.GetDetailedIpLocation(TestConfiguration.ValidIpV4);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(TestConfiguration.ValidIpV4, result.Ip);
+ Assert.NotNull(result.Location);
+ Assert.NotNull(result.Asn);
+ }
+ }
+}
diff --git a/src/clients/dotnet/Bitip.Client.Tests/TestConfiguration.cs b/src/clients/dotnet/Bitip.Client.Tests/TestConfiguration.cs
new file mode 100644
index 0000000..fff9f63
--- /dev/null
+++ b/src/clients/dotnet/Bitip.Client.Tests/TestConfiguration.cs
@@ -0,0 +1,47 @@
+// Copyright (c) 2025 Tudor Stanciu
+
+namespace Bitip.Client.Tests
+{
+ ///
+ /// Test configuration for Bitip.Client tests.
+ ///
+ public static class TestConfiguration
+ {
+ ///
+ /// Set this to true to run tests against a real Bitip API instance.
+ ///
+ public const bool UseRealApi = false;
+
+ ///
+ /// Real Bitip API base URL for integration tests.
+ /// Update this with your actual Bitip instance URL.
+ /// Example: "https://lab.code-rove.com/bitip/api/"
+ ///
+ public const string RealApiBaseUrl = "https://lab.code-rove.com/bitip/api/";
+
+ ///
+ /// Real API key for integration tests.
+ /// Update this with your actual API key when running integration tests locally.
+ /// WARNING: Never commit your real API key to source control!
+ ///
+ public const string RealApiKey = "your-api-key-here";
+
+ ///
+ /// Mock base URL used for unit tests
+ ///
+ public const string MockApiBaseUrl = "https://mock-bitip-api.test/api/";
+
+ ///
+ /// Mock API key used for unit tests
+ ///
+ public const string MockApiKey = "mock-api-key-12345";
+
+ ///
+ /// Test IP addresses
+ ///
+ public const string ValidIpV4 = "8.8.8.8";
+ public const string ValidIpV6 = "2001:4860:4860::8888";
+ public const string InvalidIp = "invalid-ip";
+ public const string PrivateIp = "192.168.1.1";
+ }
+}
diff --git a/src/clients/dotnet/Bitip.Client/Bitip.Client.csproj b/src/clients/dotnet/Bitip.Client/Bitip.Client.csproj
index ab79365..0dc0341 100644
--- a/src/clients/dotnet/Bitip.Client/Bitip.Client.csproj
+++ b/src/clients/dotnet/Bitip.Client/Bitip.Client.csproj
@@ -3,12 +3,35 @@
net9.0
enable
+
+
+ Bitip.Client
+ 1.0.0
+ Tudor Stanciu
+ Code Rove
+ Bitip.Client
+ A .NET client library for integrating with the Bitip GeoIP Service. Provides IP geolocation lookup, health checks, and version information retrieval with full async/await support.
+ Copyright © 2025 Tudor Stanciu
+
+
https://lab.code-rove.com/bitip/
+ https://lab.code-rove.com/gitea/tudor.stanciu/bitip
+ git
+
+
logo.png
README.md
- https://lab.code-rove.com/gitea/tudor.stanciu/bitip/src/branch/main/src/clients/dotnet
- Bitip Toodle
+ LICENSE
+
+
+ bitip;geoip;geolocation;ip-lookup;ip-geolocation;asn;maxmind;dotnet
$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))
+ false
+
+
+ false
+ true
+ snupkg
@@ -25,6 +48,10 @@
True
\
+
+ True
+ \
+
diff --git a/src/clients/dotnet/Bitip.Client/LICENSE b/src/clients/dotnet/Bitip.Client/LICENSE
new file mode 100644
index 0000000..688daa3
--- /dev/null
+++ b/src/clients/dotnet/Bitip.Client/LICENSE
@@ -0,0 +1,27 @@
+MIT License
+
+Copyright (c) 2025 Tudor Stanciu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+---
+
+Note: This license applies to the Bitip.Client library only. Access to the
+Bitip GeoIP Service API requires a separate API key and is subject to the
+service's own terms and conditions.
diff --git a/src/clients/dotnet/Bitip.Client/Properties/AssemblyInfo.cs b/src/clients/dotnet/Bitip.Client/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..1902204
--- /dev/null
+++ b/src/clients/dotnet/Bitip.Client/Properties/AssemblyInfo.cs
@@ -0,0 +1,5 @@
+// Copyright (c) 2025 Tudor Stanciu
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Bitip.Client.Tests")]
diff --git a/src/clients/dotnet/Bitip.Client/README.md b/src/clients/dotnet/Bitip.Client/README.md
index 2225f60..0604145 100644
--- a/src/clients/dotnet/Bitip.Client/README.md
+++ b/src/clients/dotnet/Bitip.Client/README.md
@@ -1,20 +1,275 @@
# Bitip.Client
+A .NET client library for integrating with the [Bitip GeoIP Service](https://lab.code-rove.com/bitip/). This library provides a simple and efficient way to perform IP geolocation lookups, retrieve health status, and get version information from your Bitip API instance.
+
[Bitip](https://lab.code-rove.com/gitea/tudor.stanciu/bitip#readme) is a high-performance GeoIP lookup service designed to provide accurate geolocation data for IP addresses.
--------- TO DO --------
-- You can see Bitip live here: https://lab.code-rove.com/bitip/
+## Features
-[Bitip.Client](https://lab.code-rove.com/gitea/tudor.stanciu/bitip/src/branch/main/src/clients/dotnet) is a simple .net client for Bitip. SDK is available as NuGet package on [NuGet](https://lab.code-rove.com/public-nuget-server/packages/bitip.client). Detail this section.
+- ✅ Simple IP geolocation lookup
+- ✅ Detailed IP geolocation with ASN information
+- ✅ Health check endpoint
+- ✅ Version information retrieval
+- ✅ Built-in IP address validation
+- ✅ Async/await support with CancellationToken
+- ✅ Dependency injection ready
+- ✅ Type-safe models with modern C# records
## Installation
-You can install the Bitip.Client package via NuGet Package Manager Console:
-```bash
+Install the package via NuGet Package Manager:
+```bash
+dotnet add package Bitip.Client
+```
+
+Or via Package Manager Console:
+
+```powershell
Install-Package Bitip.Client
```
-Or via .NET CLI:
-```bash
---- TO DO ---
\ No newline at end of file
+The package is available on the [public NuGet server](https://lab.code-rove.com/public-nuget-server/packages/bitip.client).
+
+## Quick Start
+
+### 1. Register the Service
+
+In your `Program.cs` or `Startup.cs`, register the Bitip client with dependency injection:
+
+```csharp
+using Bitip.Client;
+
+// Register Bitip client
+services.UseBitipClient(
+ baseUrl: "https://your-bitip-instance.com/api/",
+ apiKey: "your-api-key-here"
+);
+```
+
+### 2. Inject and Use
+
+Inject `IBitipClient` into your services or controllers:
+
+```csharp
+using Bitip.Client.Services;
+using Bitip.Client.Models;
+
+public class MyService
+{
+ private readonly IBitipClient _bitipClient;
+
+ public MyService(IBitipClient bitipClient)
+ {
+ _bitipClient = bitipClient;
+ }
+
+ public async Task GetLocationAsync(string ipAddress)
+ {
+ return await _bitipClient.GetIpLocation(ipAddress);
+ }
+}
+```
+
+## Usage Examples
+
+### Simple IP Lookup
+
+Get basic geolocation information for an IP address:
+
+```csharp
+var location = await _bitipClient.GetIpLocation("8.8.8.8");
+
+Console.WriteLine($"IP: {location.Ip}");
+Console.WriteLine($"Country: {location.Country} ({location.CountryCode})");
+Console.WriteLine($"City: {location.City}");
+Console.WriteLine($"Region: {location.Region}");
+Console.WriteLine($"Coordinates: {location.Latitude}, {location.Longitude}");
+Console.WriteLine($"Timezone: {location.Timezone}");
+Console.WriteLine($"EU Member: {location.IsInEuropeanUnion}");
+```
+
+**Response Model (`IpLocation`):**
+
+- `Ip` - The queried IP address
+- `Country` - Full country name
+- `CountryCode` - ISO country code (e.g., "US")
+- `IsInEuropeanUnion` - Boolean flag
+- `Region` - Region/state name
+- `RegionCode` - Region code (optional)
+- `City` - City name
+- `Latitude` / `Longitude` - Geographic coordinates (optional)
+- `Timezone` - IANA timezone (optional)
+- `PostalCode` - Postal/ZIP code (optional)
+- `ContinentCode` / `ContinentName` - Continent information (optional)
+- `Organization` - ISP/organization name (optional)
+
+### Detailed IP Lookup
+
+Get comprehensive geolocation data including ASN information:
+
+```csharp
+var detailedLocation = await _bitipClient.GetDetailedIpLocation("1.1.1.1");
+
+Console.WriteLine($"IP: {detailedLocation.Ip}");
+Console.WriteLine($"Country: {detailedLocation.Location.Country?.Names?["en"]}");
+Console.WriteLine($"City: {detailedLocation.Location.City?.Names?["en"]}");
+Console.WriteLine($"Latitude: {detailedLocation.Location.Location?.Latitude}");
+Console.WriteLine($"Longitude: {detailedLocation.Location.Location?.Longitude}");
+Console.WriteLine($"Timezone: {detailedLocation.Location.Location?.TimeZone}");
+Console.WriteLine($"ASN: {detailedLocation.Asn.AutonomousSystemNumber}");
+Console.WriteLine($"ISP: {detailedLocation.Asn.AutonomousSystemOrganization}");
+```
+
+**Response Model (`DetailedIpLocation`):**
+
+- `Ip` - The queried IP address
+- `Location` - Nested geolocation data
+ - `Country` - Country info with ISO code and localized names
+ - `City` - City with localized names
+ - `Subdivisions` - State/province information
+ - `Location` - Latitude, longitude, timezone
+ - `Postal` - Postal code
+ - `Continent` - Continent code and names
+ - `RegisteredCountry` - Registered country info
+ - `Traits` - Proxy and satellite provider flags
+- `Asn` - Autonomous System Number information
+ - `AutonomousSystemNumber` - ASN number
+ - `AutonomousSystemOrganization` - ISP name
+ - `IpAddress` - IP address
+ - `Network` - Network range
+
+### Health Check
+
+Check the health status of the Bitip API:
+
+```csharp
+var health = await _bitipClient.GetHealth();
+
+Console.WriteLine($"Status: {health.Status}");
+Console.WriteLine($"Service: {health.Service}");
+Console.WriteLine($"Timestamp: {health.Timestamp}");
+
+if (!string.IsNullOrEmpty(health.Error))
+{
+ Console.WriteLine($"Error: {health.Error}");
+}
+```
+
+### Version Information
+
+Retrieve the API version and build details:
+
+```csharp
+var version = await _bitipClient.GetVersion();
+
+Console.WriteLine($"Version: {version.Version}");
+Console.WriteLine($"Commit: {version.CommitHash}");
+Console.WriteLine($"Build Date: {version.BuildDate}");
+```
+
+## Error Handling
+
+The client throws standard exceptions that you should handle appropriately:
+
+```csharp
+try
+{
+ var location = await _bitipClient.GetIpLocation("invalid-ip");
+}
+catch (ArgumentException ex)
+{
+ // Invalid IP format
+ Console.WriteLine($"Invalid IP: {ex.Message}");
+}
+catch (HttpRequestException ex)
+{
+ // API error (404 not found, 503 service unavailable, etc.)
+ Console.WriteLine($"API Error: {ex.Message}");
+}
+catch (TaskCanceledException ex)
+{
+ // Request timeout
+ Console.WriteLine($"Timeout: {ex.Message}");
+}
+```
+
+### Common API Error Responses
+
+- **400 Bad Request** - Invalid IP address format or private IP
+- **404 Not Found** - IP address not found in database
+- **429 Too Many Requests** - Rate limit exceeded
+- **503 Service Unavailable** - Service under maintenance
+
+## Cancellation Support
+
+All methods support `CancellationToken` for request cancellation:
+
+```csharp
+var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
+
+try
+{
+ var location = await _bitipClient.GetIpLocation("8.8.8.8", cts.Token);
+}
+catch (OperationCanceledException)
+{
+ Console.WriteLine("Request cancelled or timed out");
+}
+```
+
+## Configuration
+
+### Base URL Format
+
+The base URL should point to your Bitip API instance. The library automatically ensures proper URL formatting:
+
+```csharp
+// All of these work correctly:
+services.UseBitipClient("https://bitip.example.com/api", "your-api-key");
+services.UseBitipClient("https://bitip.example.com/api/", "your-api-key");
+```
+
+### API Key
+
+The API key is sent via the `X-API-Key` header in all requests. Make sure your Bitip API is configured to accept your key.
+
+## Requirements
+
+- **.NET 9.0** or higher
+- **Microsoft.Extensions.DependencyInjection.Abstractions** (9.0.9+)
+- **Microsoft.Extensions.Http** (9.0.9+)
+
+## API Endpoints Used
+
+This client interacts with the following Bitip API endpoints:
+
+| Method | Endpoint | Description |
+| --------------------------- | ------------------------------ | --------------- |
+| `GetHealth()` | `GET /health` | Health check |
+| `GetVersion()` | `GET /version` | Version info |
+| `GetIpLocation(ip)` | `GET /lookup?ip={ip}` | Simple lookup |
+| `GetDetailedIpLocation(ip)` | `GET /lookup/detailed?ip={ip}` | Detailed lookup |
+
+## Best Practices
+
+1. **Reuse IBitipClient instances** - They are registered as scoped services by default
+2. **Use cancellation tokens** for requests that may take time
+3. **Handle exceptions appropriately** - Network calls can fail
+4. **Validate IPs before calling** (optional, the library validates internally)
+5. **Consider caching results** - IP geolocation data doesn't change frequently
+
+## License
+
+This project is licensed under the terms specified in the LICENSE file.
+
+## Support
+
+For issues, questions, or contributions, please visit:
+
+- **Repository**: https://lab.code-rove.com/gitea/tudor.stanciu/bitip
+- **Package**: https://lab.code-rove.com/public-nuget-server/packages/bitip.client
+
+---
+
+**Copyright © 2025 Tudor Stanciu**
diff --git a/src/clients/dotnet/Bitip.Client/RELEASE.md b/src/clients/dotnet/Bitip.Client/RELEASE.md
new file mode 100644
index 0000000..f8ff389
--- /dev/null
+++ b/src/clients/dotnet/Bitip.Client/RELEASE.md
@@ -0,0 +1,342 @@
+# Bitip.Client Release Guide
+
+This guide explains how to publish the Bitip.Client NuGet package to your private NuGet server.
+
+## Prerequisites
+
+Before publishing, ensure you have:
+
+1. **.NET 9.0 SDK** installed
+2. **NuGet CLI** or use `dotnet` CLI (recommended)
+3. **API Key** for your NuGet server (https://lab.code-rove.com/public-nuget-server)
+4. **Git** installed and repository up to date
+
+## Version Management
+
+### Semantic Versioning
+
+Bitip.Client follows [Semantic Versioning 2.0.0](https://semver.org/):
+
+```
+MAJOR.MINOR.PATCH (e.g., 1.0.0)
+```
+
+- **MAJOR**: Breaking changes to the API
+- **MINOR**: New features, backward compatible
+- **PATCH**: Bug fixes, backward compatible
+
+### When to Increment
+
+| Change Type | Version Update | Example |
+| -------------------- | ---------------- | ----------------- |
+| Breaking API changes | MAJOR | `1.0.0` → `2.0.0` |
+| New methods added | MINOR | `1.0.0` → `1.1.0` |
+| Bug fixes | PATCH | `1.0.0` → `1.0.1` |
+| Documentation only | PATCH (optional) | `1.0.0` → `1.0.1` |
+
+### How to Update Version
+
+1. **Edit `Bitip.Client.csproj`**:
+
+ ```xml
+ 1.0.0
+ ```
+
+2. **Update `ReleaseNotes.txt`** with changes:
+
+ ```
+ Version 1.0.0 (2025-10-08)
+ - Initial release
+ - IP geolocation lookup (simple and detailed)
+ - Health check endpoint
+ - Version information retrieval
+ ```
+
+3. **Commit the changes**:
+ ```powershell
+ git add Bitip.Client.csproj ReleaseNotes.txt
+ git commit -m "Bump version to 1.0.0"
+ git push
+ ```
+
+## Building the Package
+
+### 1. Clean Previous Builds
+
+```powershell
+cd d:\Git\Home\Bitip\src\clients\dotnet\Bitip.Client
+dotnet clean
+```
+
+### 2. Build in Release Mode
+
+```powershell
+dotnet build -c Release
+```
+
+### 3. Create NuGet Package
+
+```powershell
+dotnet pack -c Release -o .\nupkg
+```
+
+This creates two files in the `nupkg` folder:
+
+- `Bitip.Client.{version}.nupkg` - The main package
+- `Bitip.Client.{version}.snupkg` - Symbol package (for debugging)
+
+### 4. Verify Package Contents
+
+Before publishing, inspect the package:
+
+```powershell
+# List package contents
+nuget.exe list -Source .\nupkg
+
+# Or extract and inspect manually
+Expand-Archive .\nupkg\Bitip.Client.1.0.0.nupkg -DestinationPath .\temp
+```
+
+Ensure the package contains:
+
+- `lib/net9.0/Bitip.Client.dll`
+- `logo.png`
+- `README.md`
+- `LICENSE` (if available)
+- XML documentation file
+
+## Publishing to NuGet Server
+
+### Option 1: Using dotnet CLI (Recommended)
+
+#### First-time Setup
+
+Configure your NuGet source (one-time setup):
+
+```powershell
+dotnet nuget add source https://lab.code-rove.com/public-nuget-server/v3/index.json `
+ --name CodeRoveNuGet `
+ --username your-username `
+ --password your-api-key `
+ --store-password-in-clear-text
+```
+
+#### Publish Package
+
+```powershell
+cd d:\Git\Home\Bitip\src\clients\dotnet\Bitip.Client
+
+# Push both main and symbol packages
+dotnet nuget push .\nupkg\Bitip.Client.1.0.0.nupkg `
+ --source CodeRoveNuGet `
+ --api-key your-api-key
+```
+
+### Option 2: Using NuGet CLI
+
+```powershell
+nuget.exe push .\nupkg\Bitip.Client.1.0.0.nupkg `
+ -Source https://lab.code-rove.com/public-nuget-server `
+ -ApiKey your-api-key
+```
+
+### Verify Publication
+
+After publishing, verify the package is available:
+
+1. Visit: https://lab.code-rove.com/public-nuget-server/packages/bitip.client
+2. Or search via CLI:
+ ```powershell
+ dotnet nuget search Bitip.Client --source CodeRoveNuGet
+ ```
+
+## Complete Release Workflow
+
+Here's the full workflow from start to finish:
+
+```powershell
+# 1. Navigate to project directory
+cd d:\Git\Home\Bitip\src\clients\dotnet\Bitip.Client
+
+# 2. Update version in .csproj (manually edit)
+# 3. Update ReleaseNotes.txt (manually edit)
+
+# 4. Commit version changes
+git add Bitip.Client.csproj ReleaseNotes.txt
+git commit -m "Release version 1.0.0"
+
+# 5. Create git tag
+git tag -a v1.0.0 -m "Release version 1.0.0"
+
+# 6. Clean and build
+dotnet clean
+dotnet build -c Release
+
+# 7. Run tests (if any)
+cd ..\Bitip.Client.Tests
+dotnet test -c Release
+
+# 8. Create package
+cd ..\Bitip.Client
+dotnet pack -c Release -o .\nupkg
+
+# 9. Publish to NuGet server
+dotnet nuget push .\nupkg\Bitip.Client.1.0.0.nupkg `
+ --source CodeRoveNuGet `
+ --api-key your-api-key
+
+# 10. Push git changes and tags
+git push
+git push --tags
+```
+
+## Troubleshooting
+
+### Package Already Exists
+
+If you get "Package already exists" error:
+
+- **Solution 1**: Increment the version number (most common)
+- **Solution 2**: Delete the old package from the server (if you have permissions)
+- **Solution 3**: Unlisting the old version (if supported by your server)
+
+### Authentication Failed
+
+```
+error: Response status code does not indicate success: 401 (Unauthorized)
+```
+
+**Solutions**:
+
+- Verify your API key is correct
+- Check if the API key has expired
+- Ensure you have publish permissions on the NuGet server
+
+### Missing Dependencies
+
+If package references are not resolved:
+
+- Ensure all `PackageReference` versions are correct
+- Run `dotnet restore` before building
+- Check that your NuGet sources are configured correctly
+
+### Symbol Package Upload Failed
+
+Symbol packages (`.snupkg`) may fail if the server doesn't support them:
+
+- This is optional; the main package will still work
+- You can skip symbols with: `dotnet pack --no-symbols`
+
+## Best Practices
+
+### Before Every Release
+
+1. ✅ **Update ReleaseNotes.txt** with all changes
+2. ✅ **Run all tests** to ensure nothing is broken
+3. ✅ **Update README.md** if API changed
+4. ✅ **Review breaking changes** carefully
+5. ✅ **Create git tag** for the release
+6. ✅ **Test package locally** before publishing
+
+### Testing Locally
+
+Before publishing, test the package in a separate project:
+
+```powershell
+# In a test project
+dotnet add package Bitip.Client --source d:\Git\Home\Bitip\src\clients\dotnet\Bitip.Client\nupkg
+```
+
+### Version Checklist
+
+- [ ] Version number updated in `.csproj`
+- [ ] Release notes updated in `ReleaseNotes.txt`
+- [ ] All tests passing
+- [ ] Package built successfully
+- [ ] Package contents verified
+- [ ] Git changes committed
+- [ ] Git tag created
+- [ ] Package published to NuGet server
+- [ ] Changes pushed to Git repository
+
+## Managing API Keys
+
+### Storing API Key Securely
+
+**Option 1: Environment Variable**
+
+```powershell
+$env:NUGET_API_KEY = "your-api-key"
+dotnet nuget push .\nupkg\Bitip.Client.1.0.0.nupkg `
+ --source CodeRoveNuGet `
+ --api-key $env:NUGET_API_KEY
+```
+
+**Option 2: NuGet.Config** (store credentials)
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+**Option 3: Credential Provider** (most secure)
+
+- Use a credential provider for automatic authentication
+- Recommended for CI/CD pipelines
+
+## Automated Release (Future Enhancement)
+
+Consider setting up automated releases using GitHub Actions or similar:
+
+```yaml
+# .github/workflows/publish-nuget.yml
+name: Publish NuGet Package
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ publish:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 9.0.x
+ - name: Build
+ run: dotnet build -c Release
+ - name: Test
+ run: dotnet test -c Release
+ - name: Pack
+ run: dotnet pack -c Release -o ./nupkg
+ - name: Publish
+ run: dotnet nuget push ./nupkg/*.nupkg --source CodeRoveNuGet --api-key ${{ secrets.NUGET_API_KEY }}
+```
+
+## Quick Reference
+
+| Command | Description |
+| ----------------------------------- | ----------------------- |
+| `dotnet clean` | Clean build artifacts |
+| `dotnet build -c Release` | Build in release mode |
+| `dotnet pack -c Release -o .\nupkg` | Create NuGet package |
+| `dotnet nuget push` | Publish to NuGet server |
+| `git tag -a v1.0.0 -m "Release"` | Create git tag |
+| `git push --tags` | Push tags to remote |
+
+---
+
+**For questions or issues, contact the maintainer or visit the repository.**
+
+**Copyright © 2025 Tudor Stanciu**