diff --git a/Directory.Packages.props b/Directory.Packages.props
index 233d1feee..cb6536ed8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,6 +3,7 @@
true
+
diff --git a/docs/docs/Installation/azure.md b/docs/docs/Installation/azure.md
index 1733c96ff..4ba65c12e 100644
--- a/docs/docs/Installation/azure.md
+++ b/docs/docs/Installation/azure.md
@@ -9,21 +9,25 @@ This page is a work in progress!
:::
-Use Azure to scale BaGetter. You can store metadata on [Azure SQL Database](https://azure.microsoft.com/products/azure-sql/database/), upload packages to [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs/), and soon provide powerful search using [Azure Search](https://azure.microsoft.com/en-us/services/search/).
+Use Azure to scale BaGetter. You can store metadata on Azure [SQL Database](https://azure.microsoft.com/products/azure-sql/database/) or [Table Storage](https://azure.microsoft.com/products/storage/tables/), upload packages to [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs/), and soon provide powerful search using [Azure Search](https://azure.microsoft.com/services/search/).
## TODO
- App Service
-- Table Storage
- High availability setup
## Configure BaGetter
-You can modify BaGetter's configurations by editing the `appsettings.json` file or through [environment variables](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-8.0#non-prefixed-environment-variables). For the full list of configurations, please refer to [BaGetter's configuration](../configuration.md) guide.
+You can modify BaGetter's configurations by editing the `appsettings.json` file or through [environment variables](https://learn.microsoft.com/aspnet/core/fundamentals/configuration/?view=aspnetcore-8.0#non-prefixed-environment-variables). For the full list of configurations, please refer to [BaGetter's configuration](../configuration.md) guide.
-### Azure SQL database
+### Package Metadata Database
-Set the database type to `SqlServer` and provide a [connection string](https://learn.microsoft.com/ef/core/miscellaneous/connection-strings):
+To storage the package metadata, you can use any [Azure SQL database](https://azure.microsoft.com/products/category/databases/) (as any normal SQL DB), but also [Azure Table Storage](https://azure.microsoft.com/products/storage/tables/) in a Storage Account or Azure Cosmod DB in Table API mode.
+
+
+
+
+To use a SQL database, set the database type to the SQL provider of your choice (e.g. `SqlServer`) and provide a [connection string](https://learn.microsoft.com/ef/core/miscellaneous/connection-strings):
```json
{
@@ -38,6 +42,30 @@ Set the database type to `SqlServer` and provide a [connection string](https://l
}
```
+
+
+
+
+To use Azure Table Storage or Azure Cosmos DB in Table API mode, set the database type to `AzureTable` and provide a connection string and optionally a custom table name. If no table name is set, the default table name `Packages` will be used.
+If it doesn't exist, it will be created automatically.
+
+```json
+{
+ ...
+
+ "Database": {
+ "Type": "AzureTable",
+ "ConnectionString": "...",
+ "TableName": "my-nuget-packages"
+ },
+
+ ...
+}
+```
+
+
+
+
### Azure Blob Storage
Set the storage type to `AzureBlobStorage` and provide a container name to use and credentials:
diff --git a/src/BaGetter.Azure/AzureApplicationExtensions.cs b/src/BaGetter.Azure/AzureApplicationExtensions.cs
index 71a832616..bca4bf413 100644
--- a/src/BaGetter.Azure/AzureApplicationExtensions.cs
+++ b/src/BaGetter.Azure/AzureApplicationExtensions.cs
@@ -1,75 +1,60 @@
using System;
+using Azure.Data.Tables;
using Azure.Storage;
using Azure.Storage.Blobs;
using BaGetter.Azure;
using BaGetter.Core;
-//using Azure.Cosmos.Table;
-//using Azure.Search;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
-//using Azure.Storage.Blobs;
namespace BaGetter
{
- //using CloudStorageAccount = Azure.Storage.CloudStorageAccount;
- //using StorageCredentials = Azure.Storage.Auth.StorageCredentials;
-
- //using TableStorageAccount = Azure.Cosmos.Table.CloudStorageAccount;
-
public static class AzureApplicationExtensions
{
public static BaGetterApplication AddAzureTableDatabase(this BaGetterApplication app)
{
- throw new NotImplementedException();
-
- //app.Services.AddBaGetterOptions(nameof(BaGetterOptions.Database));
+ app.Services.AddBaGetterOptions(nameof(BaGetterOptions.Database));
- //app.Services.AddTransient();
- //app.Services.AddTransient();
- //app.Services.AddTransient();
- //app.Services.TryAddTransient(provider => provider.GetRequiredService());
- //app.Services.TryAddTransient(provider => provider.GetRequiredService());
- //app.Services.TryAddTransient(provider => provider.GetRequiredService());
+ app.Services.AddTransient();
+ app.Services.AddTransient();
+ app.Services.TryAddTransient(provider => provider.GetRequiredService());
+ app.Services.TryAddTransient(provider => provider.GetRequiredService());
+ app.Services.TryAddTransient(provider => provider.GetRequiredService());
- //app.Services.AddSingleton(provider =>
- //{
- // var options = provider.GetRequiredService>().Value;
-
- // return TableStorageAccount.Parse(options.ConnectionString);
- //});
-
- //app.Services.AddTransient(provider =>
- //{
- // var account = provider.GetRequiredService();
+ app.Services.AddSingleton(provider =>
+ {
+ var options = provider.GetRequiredService>().Value;
- // return account.CreateCloudTableClient();
- //});
+ var tableServiceClient = new TableServiceClient(options.ConnectionString);
+ tableServiceClient.CreateTableIfNotExists(options.TableName);
+ return tableServiceClient;
+ });
- //app.Services.AddProvider((provider, config) =>
- //{
- // if (!config.HasDatabaseType("AzureTable")) return null;
+ app.Services.AddProvider((provider, config) =>
+ {
+ if (!config.HasDatabaseType("AzureTable")) return null;
- // return provider.GetRequiredService();
- //});
+ return provider.GetRequiredService();
+ });
- //app.Services.AddProvider((provider, config) =>
- //{
- // if (!config.HasSearchType("Database")) return null;
- // if (!config.HasDatabaseType("AzureTable")) return null;
+ app.Services.AddProvider((provider, config) =>
+ {
+ if (!config.HasSearchType("Database")) return null;
+ if (!config.HasDatabaseType("AzureTable")) return null;
- // return provider.GetRequiredService();
- //});
+ return provider.GetRequiredService();
+ });
- //app.Services.AddProvider((provider, config) =>
- //{
- // if (!config.HasSearchType("Database")) return null;
- // if (!config.HasDatabaseType("AzureTable")) return null;
+ app.Services.AddProvider((provider, config) =>
+ {
+ if (!config.HasSearchType("Database")) return null;
+ if (!config.HasDatabaseType("AzureTable")) return null;
- // return provider.GetRequiredService();
- //});
+ return provider.GetRequiredService();
+ });
- //return app;
+ return app;
}
public static BaGetterApplication AddAzureTableDatabase(
diff --git a/src/BaGetter.Azure/BaGetter.Azure.csproj b/src/BaGetter.Azure/BaGetter.Azure.csproj
index a88d1d3e1..c13748c08 100644
--- a/src/BaGetter.Azure/BaGetter.Azure.csproj
+++ b/src/BaGetter.Azure/BaGetter.Azure.csproj
@@ -8,10 +8,10 @@
+
-
@@ -21,6 +21,5 @@
-
diff --git a/src/BaGetter.Azure/Configuration/AzureTableOptions.cs b/src/BaGetter.Azure/Configuration/AzureTableOptions.cs
index 9090cff0d..60351e1e5 100644
--- a/src/BaGetter.Azure/Configuration/AzureTableOptions.cs
+++ b/src/BaGetter.Azure/Configuration/AzureTableOptions.cs
@@ -6,5 +6,6 @@ public class AzureTableOptions
{
[Required]
public string ConnectionString { get; set; }
+ public string TableName { get; set; } = "Packages";
}
}
diff --git a/src/BaGetter.Azure/Extensions/StorageExceptionExtensions.cs b/src/BaGetter.Azure/Extensions/StorageExceptionExtensions.cs
index 4a06c1f80..115c808f8 100644
--- a/src/BaGetter.Azure/Extensions/StorageExceptionExtensions.cs
+++ b/src/BaGetter.Azure/Extensions/StorageExceptionExtensions.cs
@@ -21,9 +21,9 @@ public static bool IsAlreadyExistsException(this RequestFailedException e)
// return e?.RequestInformation?.HttpStatusCode == (int?)HttpStatusCode.Conflict;
//}
- //public static bool IsPreconditionFailedException(this TableStorageException e)
- //{
- // return e?.RequestInformation?.HttpStatusCode == (int?)HttpStatusCode.PreconditionFailed;
- //}
+ public static bool IsPreconditionFailedException(this RequestFailedException e)
+ {
+ return e?.Status == (int?)HttpStatusCode.PreconditionFailed;
+ }
}
}
diff --git a/src/BaGetter.Azure/Table/PackageEntity.cs b/src/BaGetter.Azure/Table/PackageEntity.cs
index f5bbc0af3..6e2daed84 100644
--- a/src/BaGetter.Azure/Table/PackageEntity.cs
+++ b/src/BaGetter.Azure/Table/PackageEntity.cs
@@ -1,20 +1,17 @@
using System;
+using Azure;
+using Azure.Data.Tables;
using BaGetter.Core;
-using Microsoft.Azure.Cosmos.Table;
namespace BaGetter.Azure
{
///
/// The Azure Table Storage entity that maps to a .
- /// The is the and
- /// the is the .
+ /// The is the and
+ /// the is the .
///
- public class PackageEntity : TableEntity, IDownloadCount, IListed
+ public class PackageEntity : ITableEntity, IDownloadCount, IListed
{
- public PackageEntity()
- {
- }
-
public string Id { get; set; }
public string NormalizedVersion { get; set; }
public string OriginalVersion { get; set; }
@@ -45,6 +42,10 @@ public PackageEntity()
public string Dependencies { get; set; }
public string PackageTypes { get; set; }
public string TargetFrameworks { get; set; }
+ public string PartitionKey { get; set; }
+ public string RowKey { get; set; }
+ public DateTimeOffset? Timestamp { get; set; }
+ public ETag ETag { get; set; }
}
///
@@ -68,30 +69,30 @@ public class PackageTypeModel
///
/// The Azure Table Storage entity to update the column.
- /// The is the and
- /// the is the .
+ /// The is the and
+ /// the is the .
///
- public class PackageListingEntity : TableEntity, IListed
+ public class PackageListingEntity : ITableEntity, IListed
{
- public PackageListingEntity()
- {
- }
-
public bool Listed { get; set; }
+ public string PartitionKey { get; set; }
+ public string RowKey { get; set; }
+ public DateTimeOffset? Timestamp { get; set; }
+ public ETag ETag { get; set; }
}
///
/// The Azure Table Storage entity to update the column.
- /// The is the and
- /// the is the .
+ /// The is the and
+ /// the is the .
///
- public class PackageDownloadsEntity : TableEntity, IDownloadCount
+ public class PackageDownloadsEntity : ITableEntity, IDownloadCount
{
- public PackageDownloadsEntity()
- {
- }
-
public long Downloads { get; set; }
+ public string PartitionKey { get; set; }
+ public string RowKey { get; set; }
+ public DateTimeOffset? Timestamp { get; set; }
+ public ETag ETag { get; set; }
}
internal interface IListed
diff --git a/src/BaGetter.Azure/Table/PackageEntityExtensions.cs b/src/BaGetter.Azure/Table/PackageEntityExtensions.cs
index 94c9c8d03..8a23231b6 100644
--- a/src/BaGetter.Azure/Table/PackageEntityExtensions.cs
+++ b/src/BaGetter.Azure/Table/PackageEntityExtensions.cs
@@ -1,8 +1,8 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text.Json;
using BaGetter.Core;
-using Newtonsoft.Json;
namespace BaGetter.Azure
{
@@ -12,12 +12,10 @@ public static Package AsPackage(this PackageEntity entity)
{
return new Package
{
- Id = entity.Id,
+ Id = entity.PartitionKey,
NormalizedVersionString = entity.NormalizedVersion,
OriginalVersionString = entity.OriginalVersion,
-
- // TODO: Convert to System.Text.Json
- Authors = JsonConvert.DeserializeObject(entity.Authors),
+ Authors = JsonSerializer.Deserialize(entity.Authors),
Description = entity.Description,
Downloads = entity.Downloads,
HasReadme = entity.HasReadme,
@@ -37,7 +35,7 @@ public static Package AsPackage(this PackageEntity entity)
ProjectUrl = ParseUri(entity.ProjectUrl),
RepositoryUrl = ParseUri(entity.RepositoryUrl),
RepositoryType = entity.RepositoryType,
- Tags = JsonConvert.DeserializeObject(entity.Tags),
+ Tags = JsonSerializer.Deserialize(entity.Tags),
Dependencies = ParseDependencies(entity.Dependencies),
PackageTypes = ParsePackageTypes(entity.PackageTypes),
TargetFrameworks = ParseTargetFrameworks(entity.TargetFrameworks),
@@ -51,8 +49,7 @@ private static Uri ParseUri(string input)
private static List ParseDependencies(string input)
{
- // TODO: Convert to System.Text.Json
- return JsonConvert.DeserializeObject>(input)
+ return JsonSerializer.Deserialize>(input)
.Select(e => new PackageDependency
{
Id = e.Id,
@@ -64,8 +61,7 @@ private static List ParseDependencies(string input)
private static List ParsePackageTypes(string input)
{
- // TODO: Convert to System.Text.Json
- return JsonConvert.DeserializeObject>(input)
+ return JsonSerializer.Deserialize>(input)
.Select(e => new PackageType
{
Name = e.Name,
@@ -76,8 +72,7 @@ private static List ParsePackageTypes(string input)
private static List ParseTargetFrameworks(string targetFrameworks)
{
- // TODO: Convert to System.Text.Json
- return JsonConvert.DeserializeObject>(targetFrameworks)
+ return JsonSerializer.Deserialize>(targetFrameworks)
.Select(f => new TargetFramework { Moniker = f })
.ToList();
}
diff --git a/src/BaGetter.Azure/Table/TableOperationBuilder.cs b/src/BaGetter.Azure/Table/TableOperationBuilder.cs
index 6637ea048..979edba8c 100644
--- a/src/BaGetter.Azure/Table/TableOperationBuilder.cs
+++ b/src/BaGetter.Azure/Table/TableOperationBuilder.cs
@@ -1,20 +1,16 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
using BaGetter.Core;
-using Microsoft.Azure.Cosmos.Table;
using Newtonsoft.Json;
-using NuGet.Versioning;
namespace BaGetter.Azure
{
- [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Would be a breaking change since it's part of the public API")]
public class TableOperationBuilder
{
- public TableOperation AddPackage(Package package)
+ public static PackageEntity GetEntity(Package package)
{
- if (package == null) throw new ArgumentNullException(nameof(package));
+ ArgumentNullException.ThrowIfNull(package);
var version = package.Version;
var normalizedVersion = version.ToNormalizedString();
@@ -53,57 +49,10 @@ public TableOperation AddPackage(Package package)
TargetFrameworks = SerializeList(package.TargetFrameworks, f => f.Moniker)
};
- return TableOperation.Insert(entity);
+ return entity;
}
- public TableOperation UpdateDownloads(string packageId, NuGetVersion packageVersion, long downloads)
- {
- var entity = new PackageDownloadsEntity();
-
- entity.PartitionKey = packageId.ToLowerInvariant();
- entity.RowKey = packageVersion.ToNormalizedString().ToLowerInvariant();
- entity.Downloads = downloads;
- entity.ETag = "*";
-
- return TableOperation.Merge(entity);
- }
-
- public TableOperation HardDeletePackage(string packageId, NuGetVersion packageVersion)
- {
- var entity = new PackageEntity();
-
- entity.PartitionKey = packageId.ToLowerInvariant();
- entity.RowKey = packageVersion.ToNormalizedString().ToLowerInvariant();
- entity.ETag = "*";
-
- return TableOperation.Delete(entity);
- }
-
- public TableOperation UnlistPackage(string packageId, NuGetVersion packageVersion)
- {
- var entity = new PackageListingEntity();
-
- entity.PartitionKey = packageId.ToLowerInvariant();
- entity.RowKey = packageVersion.ToNormalizedString().ToLowerInvariant();
- entity.Listed = false;
- entity.ETag = "*";
-
- return TableOperation.Merge(entity);
- }
-
- public TableOperation RelistPackage(string packageId, NuGetVersion packageVersion)
- {
- var entity = new PackageListingEntity();
-
- entity.PartitionKey = packageId.ToLowerInvariant();
- entity.RowKey = packageVersion.ToNormalizedString().ToLowerInvariant();
- entity.Listed = true;
- entity.ETag = "*";
-
- return TableOperation.Merge(entity);
- }
-
- private static string SerializeList(IReadOnlyList objects, Func map)
+ private static string SerializeList(IEnumerable objects, Func map)
{
var data = objects.Select(map).ToList();
diff --git a/src/BaGetter.Azure/Table/TablePackageDatabase.cs b/src/BaGetter.Azure/Table/TablePackageDatabase.cs
index 0adacf525..73337254c 100644
--- a/src/BaGetter.Azure/Table/TablePackageDatabase.cs
+++ b/src/BaGetter.Azure/Table/TablePackageDatabase.cs
@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net;
using System.Threading;
using System.Threading.Tasks;
+using Azure;
+using Azure.Data.Tables;
using BaGetter.Core;
-using Microsoft.Azure.Cosmos.Table;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using NuGet.Versioning;
namespace BaGetter.Azure
@@ -15,32 +18,32 @@ namespace BaGetter.Azure
///
public class TablePackageDatabase : IPackageDatabase
{
- private const string TableName = "Packages";
private const int MaxPreconditionFailures = 5;
- private readonly TableOperationBuilder _operationBuilder;
- private readonly CloudTable _table;
+ private readonly TableClient _table;
private readonly ILogger _logger;
public TablePackageDatabase(
- TableOperationBuilder operationBuilder,
- CloudTableClient client,
- ILogger logger)
+ TableServiceClient client,
+ ILogger logger,
+ IOptions options)
{
- _operationBuilder = operationBuilder ?? throw new ArgumentNullException(nameof(operationBuilder));
- _table = client?.GetTableReference(TableName) ?? throw new ArgumentNullException(nameof(client));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ ArgumentNullException.ThrowIfNull(client);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ _table = client.GetTableClient(options.Value.TableName);
+ _logger = logger;
}
public async Task AddAsync(Package package, CancellationToken cancellationToken)
{
try
{
- var operation = _operationBuilder.AddPackage(package);
+ var entity = TableOperationBuilder.GetEntity(package);
- await _table.ExecuteAsync(operation, cancellationToken);
+ await _table.AddEntityAsync(entity, cancellationToken);
}
- catch (StorageException e) when (e.IsAlreadyExistsException())
+ catch (RequestFailedException e) when (e.IsAlreadyExistsException())
{
return PackageAddResult.PackageAlreadyExists;
}
@@ -59,24 +62,32 @@ public async Task AddDownloadAsync(
{
try
{
- var operation = TableOperation.Retrieve(
- id.ToLowerInvariant(),
- version.ToNormalizedString().ToLowerInvariant());
-
- var result = await _table.ExecuteAsync(operation, cancellationToken);
- var entity = result.Result as PackageDownloadsEntity;
+ var result = await _table.GetEntityIfExistsAsync(id, version.ToNormalizedString().ToLowerInvariant(), cancellationToken: cancellationToken);
- if (entity == null)
+ if (!result.HasValue)
{
return;
}
+ var entity = result.Value;
+
entity.Downloads += 1;
- await _table.ExecuteAsync(TableOperation.Merge(entity), cancellationToken);
+ var updateResponse = await _table.UpdateEntityAsync(entity, entity.ETag, TableUpdateMode.Merge, cancellationToken);
+
+ // Not sure if there's gonna be an exception here so check both ways just in case
+ if(updateResponse.Status == (int?)HttpStatusCode.PreconditionFailed && attempt < MaxPreconditionFailures)
+ {
+ attempt++;
+ _logger.LogWarning(
+ "Retrying due to precondition failure, attempt {Attempt} of {MaxPreconditionFailures}",
+ attempt, MaxPreconditionFailures);
+ continue;
+ }
+
return;
}
- catch (StorageException e)
+ catch (RequestFailedException e)
when (attempt < MaxPreconditionFailures && e.IsPreconditionFailedException())
{
attempt++;
@@ -90,15 +101,18 @@ public async Task AddDownloadAsync(
public async Task ExistsAsync(string id, CancellationToken cancellationToken)
{
- var filter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, id.ToLowerInvariant());
- var query = new TableQuery()
- .Select(MinimalColumnSet)
- .Where(filter)
- .Take(1);
+ var query = _table.QueryAsync(p =>
+ p.PartitionKey.Equals(id, StringComparison.InvariantCultureIgnoreCase),
+ 1,
+ MinimalColumnSet,
+ cancellationToken);
- var result = await _table.ExecuteQuerySegmentedAsync(query, token: null, cancellationToken);
+ await foreach(var _ in query)
+ {
+ return true;
+ }
- return result.Results.Any();
+ return false;
}
public async Task ExistsAsync(
@@ -106,44 +120,34 @@ public async Task ExistsAsync(
NuGetVersion version,
CancellationToken cancellationToken)
{
- var operation = TableOperation.Retrieve(
- id.ToLowerInvariant(),
- version.ToNormalizedString().ToLowerInvariant(),
- MinimalColumnSet);
-
- var execution = await _table.ExecuteAsync(operation, cancellationToken);
+ var query = _table.QueryAsync(p =>
+ p.PartitionKey.Equals(id, StringComparison.InvariantCultureIgnoreCase) &&
+ p.RowKey.Equals(version.ToNormalizedString(), StringComparison.InvariantCultureIgnoreCase),
+ 1,
+ MinimalColumnSet,
+ cancellationToken);
+
+ await foreach (var _ in query)
+ {
+ return true;
+ }
- return execution.Result is PackageEntity;
+ return false;
}
public async Task> FindAsync(string id, bool includeUnlisted, CancellationToken cancellationToken)
{
- var filter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, id.ToLowerInvariant());
- if (!includeUnlisted)
- {
- filter = TableQuery.CombineFilters(
- filter,
- TableOperators.And,
- TableQuery.GenerateFilterConditionForBool(nameof(PackageEntity.Listed), QueryComparisons.Equal, true));
- }
+ const int maxPerPage = 500;
+ var query =
+ includeUnlisted
+ ? _table.QueryAsync(p => p.PartitionKey.Equals(id, StringComparison.InvariantCultureIgnoreCase), maxPerPage, cancellationToken: cancellationToken)
+ : _table.QueryAsync(p => p.PartitionKey.Equals(id, StringComparison.InvariantCultureIgnoreCase) && p.Listed, maxPerPage, cancellationToken: cancellationToken);
- var query = new TableQuery().Where(filter);
var results = new List();
-
- // Request 500 results at a time from the server.
- TableContinuationToken token = null;
- query.TakeCount = 500;
-
- do
+ await foreach (var entity in query)
{
- var segment = await _table.ExecuteQuerySegmentedAsync(query, token, cancellationToken);
-
- token = segment.ContinuationToken;
-
- // Write out the properties for each entity returned.
- results.AddRange(segment.Results.Select(r => r.AsPackage()));
+ results.Add(entity.AsPackage());
}
- while (token != null);
return results.OrderBy(p => p.Version).ToList();
}
@@ -154,18 +158,15 @@ public async Task FindOrNullAsync(
bool includeUnlisted,
CancellationToken cancellationToken)
{
- var operation = TableOperation.Retrieve(
- id.ToLowerInvariant(),
- version.ToNormalizedString().ToLowerInvariant());
+ var result = await _table.GetEntityIfExistsAsync(id, version.ToNormalizedString(), cancellationToken: cancellationToken);
- var result = await _table.ExecuteAsync(operation, cancellationToken);
- var entity = result.Result as PackageEntity;
-
- if (entity == null)
+ if (!result.HasValue)
{
return null;
}
+ var entity = result.Value;
+
// Filter out the package if it's unlisted.
if (!includeUnlisted && !entity.Listed)
{
@@ -177,39 +178,46 @@ public async Task FindOrNullAsync(
public async Task HardDeletePackageAsync(string id, NuGetVersion version, CancellationToken cancellationToken)
{
- return await TryUpdatePackageAsync(
- _operationBuilder.HardDeletePackage(id, version),
- cancellationToken);
+ var result = await _table.DeleteEntityAsync(id, version.ToNormalizedString().ToLowerInvariant(), cancellationToken: cancellationToken);
+ return !result.IsError;
}
public async Task RelistPackageAsync(string id, NuGetVersion version, CancellationToken cancellationToken)
{
- return await TryUpdatePackageAsync(
- _operationBuilder.RelistPackage(id, version),
- cancellationToken);
+ var result = await _table.GetEntityIfExistsAsync(id, version.ToNormalizedString().ToLowerInvariant(), cancellationToken: cancellationToken);
+
+ if (!result.HasValue)
+ {
+ return false;
+ }
+
+ var entity = result.Value;
+
+ entity.Listed = true;
+
+ await _table.UpdateEntityAsync(entity, ETag.All, TableUpdateMode.Merge, cancellationToken);
+
+ return true;
}
public async Task UnlistPackageAsync(string id, NuGetVersion version, CancellationToken cancellationToken)
{
- return await TryUpdatePackageAsync(
- _operationBuilder.UnlistPackage(id, version),
- cancellationToken);
- }
+ var result = await _table.GetEntityIfExistsAsync(id, version.ToNormalizedString().ToLowerInvariant(), cancellationToken: cancellationToken);
- private static List MinimalColumnSet => new List { "PartitionKey" };
-
- private async Task TryUpdatePackageAsync(TableOperation operation, CancellationToken cancellationToken)
- {
- try
- {
- await _table.ExecuteAsync(operation, cancellationToken);
- }
- catch (StorageException e) when (e.IsNotFoundException())
+ if (!result.HasValue)
{
return false;
}
+ var entity = result.Value;
+
+ entity.Listed = false;
+
+ await _table.UpdateEntityAsync(entity, ETag.All, TableUpdateMode.Merge, cancellationToken);
+
return true;
}
+
+ private static List MinimalColumnSet => ["PartitionKey"];
}
}
diff --git a/src/BaGetter.Azure/Table/TableSearchService.cs b/src/BaGetter.Azure/Table/TableSearchService.cs
index 2e00cdf03..4f2d0508d 100644
--- a/src/BaGetter.Azure/Table/TableSearchService.cs
+++ b/src/BaGetter.Azure/Table/TableSearchService.cs
@@ -3,9 +3,10 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Azure;
+using Azure.Data.Tables;
using BaGetter.Core;
using BaGetter.Protocol.Models;
-using Microsoft.Azure.Cosmos.Table;
namespace BaGetter.Azure
{
@@ -13,15 +14,18 @@ public class TableSearchService : ISearchService
{
private const string TableName = "Packages";
- private readonly CloudTable _table;
+ private readonly TableClient _table;
private readonly ISearchResponseBuilder _responseBuilder;
public TableSearchService(
- CloudTableClient client,
+ TableServiceClient client,
ISearchResponseBuilder responseBuilder)
{
- _table = client?.GetTableReference(TableName) ?? throw new ArgumentNullException(nameof(client));
- _responseBuilder = responseBuilder ?? throw new ArgumentNullException(nameof(responseBuilder));
+ ArgumentNullException.ThrowIfNull(client, nameof(client));
+ ArgumentNullException.ThrowIfNull(responseBuilder, nameof(responseBuilder));
+
+ _table = client.GetTableClient(TableName);
+ _responseBuilder = responseBuilder;
}
public async Task SearchAsync(
@@ -82,11 +86,9 @@ private async Task> SearchAsync(
bool includeSemVer2,
CancellationToken cancellationToken)
{
- var query = new TableQuery();
- query = query.Where(GenerateSearchFilter(searchText, includePrerelease, includeSemVer2));
- query.TakeCount = 500;
+ var query = _table.QueryAsync(GenerateSearchFilter(searchText, includePrerelease, includeSemVer2), cancellationToken: cancellationToken);
- var results = await LoadPackagesAsync(query, maxPartitions: skip + take, cancellationToken);
+ var results = await LoadPackagesAsync(query, maxPartitions: skip + take);
return results
.GroupBy(p => p.Id, StringComparer.OrdinalIgnoreCase)
@@ -96,39 +98,30 @@ private async Task> SearchAsync(
.ToList();
}
- private async Task> LoadPackagesAsync(
- TableQuery query,
- int maxPartitions,
- CancellationToken cancellationToken)
+ private static async Task> LoadPackagesAsync(
+ AsyncPageable query,
+ int maxPartitions)
{
var results = new List();
var partitions = 0;
string lastPartitionKey = null;
- TableContinuationToken token = null;
- do
- {
- var segment = await _table.ExecuteQuerySegmentedAsync(query, token, cancellationToken);
-
- token = segment.ContinuationToken;
- foreach (var result in segment.Results)
+ await foreach (var result in query)
+ {
+ if (lastPartitionKey != result.PartitionKey)
{
- if (lastPartitionKey != result.PartitionKey)
- {
- lastPartitionKey = result.PartitionKey;
- partitions++;
+ lastPartitionKey = result.PartitionKey;
+ partitions++;
- if (partitions > maxPartitions)
- {
- break;
- }
+ if (partitions > maxPartitions)
+ {
+ break;
}
-
- results.Add(result.AsPackage());
}
+
+ results.Add(result.AsPackage());
}
- while (token != null);
return results;
}
@@ -139,45 +132,27 @@ private static string GenerateSearchFilter(string searchText, bool includePrerel
if (!string.IsNullOrWhiteSpace(searchText))
{
- // Filter to rows where the "searchText" prefix matches on the partition key.
var prefix = searchText.TrimEnd().Split(separator: null).Last();
var prefixLower = prefix;
var prefixUpper = prefix + "~";
- var partitionLowerFilter = TableQuery.GenerateFilterCondition(
- "PartitionKey",
- QueryComparisons.GreaterThanOrEqual,
- prefixLower);
-
- var partitionUpperFilter = TableQuery.GenerateFilterCondition(
- "PartitionKey",
- QueryComparisons.LessThanOrEqual,
- prefixUpper);
+ var partitionLowerFilter = $"PartitionKey ge '{prefixLower}'";
+ var partitionUpperFilter = $"PartitionKey le '{prefixUpper}'";
result = GenerateAnd(partitionLowerFilter, partitionUpperFilter);
}
- // Filter to rows that are listed.
- result = GenerateAnd(
- result,
- GenerateIsTrue(nameof(PackageEntity.Listed)));
+ result = GenerateAnd(result, "Listed eq true");
if (!includePrerelease)
{
- result = GenerateAnd(
- result,
- GenerateIsFalse(nameof(PackageEntity.IsPrerelease)));
+ result = GenerateAnd(result, "IsPrerelease eq false");
}
if (!includeSemVer2)
{
- result = GenerateAnd(
- result,
- TableQuery.GenerateFilterConditionForInt(
- nameof(PackageEntity.SemVerLevel),
- QueryComparisons.Equal,
- 0));
+ result = GenerateAnd(result, "SemVerLevel eq 0");
}
return result;
@@ -186,23 +161,7 @@ string GenerateAnd(string left, string right)
{
if (string.IsNullOrEmpty(left)) return right;
- return TableQuery.CombineFilters(left, TableOperators.And, right);
- }
-
- string GenerateIsTrue(string propertyName)
- {
- return TableQuery.GenerateFilterConditionForBool(
- propertyName,
- QueryComparisons.Equal,
- givenValue: true);
- }
-
- string GenerateIsFalse(string propertyName)
- {
- return TableQuery.GenerateFilterConditionForBool(
- propertyName,
- QueryComparisons.Equal,
- givenValue: false);
+ return $"({left}) and ({right})";
}
}
}
diff --git a/src/BaGetter.Core/Content/IPackageContentService.cs b/src/BaGetter.Core/Content/IPackageContentService.cs
index fb221e14e..1c4018bda 100644
--- a/src/BaGetter.Core/Content/IPackageContentService.cs
+++ b/src/BaGetter.Core/Content/IPackageContentService.cs
@@ -28,8 +28,8 @@ Task GetPackageVersionsOrNullAsync(
/// Download a package, or null if the package does not exist.
/// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
///
- /// The package ID.
- /// The package's version.
+ /// The package ID, e.g. "BaGetter.Protocol".
+ /// The package's version, e.g. "1.2.0".
/// A token to cancel the task.
///
/// The package's content stream, or null if the package does not exist. The stream may not be seekable.
diff --git a/src/BaGetter.Web/Controllers/PackageContentController.cs b/src/BaGetter.Web/Controllers/PackageContentController.cs
index b8a17e988..2d995ee1b 100644
--- a/src/BaGetter.Web/Controllers/PackageContentController.cs
+++ b/src/BaGetter.Web/Controllers/PackageContentController.cs
@@ -10,7 +10,7 @@ namespace BaGetter.Web;
///
/// The Package Content resource, used to download content from packages.
-/// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource
+/// See: https://docs.microsoft.com/nuget/api/package-base-address-resource
///
public class PackageContentController : Controller
{
@@ -18,7 +18,9 @@ public class PackageContentController : Controller
public PackageContentController(IPackageContentService content)
{
- _content = content ?? throw new ArgumentNullException(nameof(content));
+ ArgumentNullException.ThrowIfNull(content);
+
+ _content = content;
}
public async Task> GetPackageVersionsAsync(string id, CancellationToken cancellationToken)
@@ -32,6 +34,13 @@ public async Task> GetPackageVersionsAsync
return versions;
}
+ ///
+ /// Download a specific package version.
+ ///
+ /// Package id, e.g. "BaGetter.Protocol".
+ /// Package version, e.g. "1.2.0".
+ /// A token to cancel the task.
+ /// The requested package in an octet stream, or 404 not found if the package isn't found.
public async Task DownloadPackageAsync(string id, string version, CancellationToken cancellationToken)
{
if (!NuGetVersion.TryParse(version, out var nugetVersion))
diff --git a/src/BaGetter/Startup.cs b/src/BaGetter/Startup.cs
index a87533dc2..e6b63ba57 100644
--- a/src/BaGetter/Startup.cs
+++ b/src/BaGetter/Startup.cs
@@ -52,7 +52,7 @@ private void ConfigureBaGetterApplication(BaGetterApplication app)
app.AddStatistics();
// Add database providers.
- //app.AddAzureTableDatabase();
+ app.AddAzureTableDatabase();
app.AddMySqlDatabase();
app.AddPostgreSqlDatabase();
app.AddSqliteDatabase();