Skip to content

Commit

Permalink
feat(Database): Add Azure Table Storage API
Browse files Browse the repository at this point in the history
This supports Azure Table Storage, and the Azure Cosmos DB Table API.
  • Loading branch information
Regenhardt committed Apr 5, 2024
1 parent 4851f9e commit 96c3041
Show file tree
Hide file tree
Showing 14 changed files with 243 additions and 308 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Azure.Data.Tables" Version="12.8.3" />
<PackageVersion Include="Azure.Search.Documents" Version="11.5.1" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.19.1" />
<PackageVersion Include="Azure.Storage.Common" Version="12.18.1" />
Expand Down
38 changes: 33 additions & 5 deletions docs/docs/Installation/azure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Tabs>
<TabItem value="sql" label="SQL Database" default>

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
{
Expand All @@ -38,6 +42,30 @@ Set the database type to `SqlServer` and provide a [connection string](https://l
}
```

</TabItem>

<TabItem value="table" label="Table Storage">

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"
},

...
}
```

</TabItem>
</Tabs>

### Azure Blob Storage

Set the storage type to `AzureBlobStorage` and provide a container name to use and credentials:
Expand Down
79 changes: 32 additions & 47 deletions src/BaGetter.Azure/AzureApplicationExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<AzureTableOptions>(nameof(BaGetterOptions.Database));
app.Services.AddBaGetterOptions<AzureTableOptions>(nameof(BaGetterOptions.Database));

//app.Services.AddTransient<TablePackageDatabase>();
//app.Services.AddTransient<TableOperationBuilder>();
//app.Services.AddTransient<TableSearchService>();
//app.Services.TryAddTransient<IPackageDatabase>(provider => provider.GetRequiredService<TablePackageDatabase>());
//app.Services.TryAddTransient<ISearchService>(provider => provider.GetRequiredService<TableSearchService>());
//app.Services.TryAddTransient<ISearchIndexer>(provider => provider.GetRequiredService<NullSearchIndexer>());
app.Services.AddTransient<TablePackageDatabase>();
app.Services.AddTransient<TableSearchService>();
app.Services.TryAddTransient<IPackageDatabase>(provider => provider.GetRequiredService<TablePackageDatabase>());
app.Services.TryAddTransient<ISearchService>(provider => provider.GetRequiredService<TableSearchService>());
app.Services.TryAddTransient<ISearchIndexer>(provider => provider.GetRequiredService<NullSearchIndexer>());

//app.Services.AddSingleton(provider =>
//{
// var options = provider.GetRequiredService<IOptions<AzureTableOptions>>().Value;

// return TableStorageAccount.Parse(options.ConnectionString);
//});

//app.Services.AddTransient(provider =>
//{
// var account = provider.GetRequiredService<TableStorageAccount>();
app.Services.AddSingleton(provider =>
{
var options = provider.GetRequiredService<IOptions<AzureTableOptions>>().Value;

// return account.CreateCloudTableClient();
//});
var tableServiceClient = new TableServiceClient(options.ConnectionString);
tableServiceClient.CreateTableIfNotExists(options.TableName);
return tableServiceClient;
});

//app.Services.AddProvider<IPackageDatabase>((provider, config) =>
//{
// if (!config.HasDatabaseType("AzureTable")) return null;
app.Services.AddProvider<IPackageDatabase>((provider, config) =>
{
if (!config.HasDatabaseType("AzureTable")) return null;

// return provider.GetRequiredService<TablePackageDatabase>();
//});
return provider.GetRequiredService<TablePackageDatabase>();
});

//app.Services.AddProvider<ISearchService>((provider, config) =>
//{
// if (!config.HasSearchType("Database")) return null;
// if (!config.HasDatabaseType("AzureTable")) return null;
app.Services.AddProvider<ISearchService>((provider, config) =>
{
if (!config.HasSearchType("Database")) return null;
if (!config.HasDatabaseType("AzureTable")) return null;

// return provider.GetRequiredService<TableSearchService>();
//});
return provider.GetRequiredService<TableSearchService>();
});

//app.Services.AddProvider<ISearchIndexer>((provider, config) =>
//{
// if (!config.HasSearchType("Database")) return null;
// if (!config.HasDatabaseType("AzureTable")) return null;
app.Services.AddProvider<ISearchIndexer>((provider, config) =>
{
if (!config.HasSearchType("Database")) return null;
if (!config.HasDatabaseType("AzureTable")) return null;

// return provider.GetRequiredService<NullSearchIndexer>();
//});
return provider.GetRequiredService<NullSearchIndexer>();
});

//return app;
return app;
}

public static BaGetterApplication AddAzureTableDatabase(
Expand Down
3 changes: 1 addition & 2 deletions src/BaGetter.Azure/BaGetter.Azure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Data.Tables" />
<PackageReference Include="Azure.Search.Documents" />
<PackageReference Include="Azure.Storage.Blobs" />
<PackageReference Include="Azure.Storage.Common" />
<PackageReference Include="Microsoft.Azure.Cosmos" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
</ItemGroup>

Expand All @@ -21,6 +21,5 @@

<ItemGroup>
<Compile Remove="Search/*.cs" />
<Compile Remove="Table/*.cs" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions src/BaGetter.Azure/Configuration/AzureTableOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ public class AzureTableOptions
{
[Required]
public string ConnectionString { get; set; }
public string TableName { get; set; } = "Packages";
}
}
8 changes: 4 additions & 4 deletions src/BaGetter.Azure/Extensions/StorageExceptionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
45 changes: 23 additions & 22 deletions src/BaGetter.Azure/Table/PackageEntity.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
using System;
using Azure;
using Azure.Data.Tables;
using BaGetter.Core;
using Microsoft.Azure.Cosmos.Table;

namespace BaGetter.Azure
{
/// <summary>
/// The Azure Table Storage entity that maps to a <see cref="Package"/>.
/// The <see cref="TableEntity.PartitionKey"/> is the <see cref="Package.Id"/> and
/// the <see cref="TableEntity.RowKey"/> is the <see cref="Package.Version"/>.
/// The <see cref="ITableEntity.PartitionKey"/> is the <see cref="Package.Id"/> and
/// the <see cref="ITableEntity.RowKey"/> is the <see cref="Package.Version"/>.
/// </summary>
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; }
Expand Down Expand Up @@ -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; }
}

/// <summary>
Expand All @@ -68,30 +69,30 @@ public class PackageTypeModel

/// <summary>
/// The Azure Table Storage entity to update the <see cref="Package.Listed"/> column.
/// The <see cref="TableEntity.PartitionKey"/> is the <see cref="Package.Id"/> and
/// the <see cref="TableEntity.RowKey"/> is the <see cref="Package.Version"/>.
/// The <see cref="ITableEntity.PartitionKey"/> is the <see cref="Package.Id"/> and
/// the <see cref="ITableEntity.RowKey"/> is the <see cref="Package.Version"/>.
/// </summary>
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; }
}

/// <summary>
/// The Azure Table Storage entity to update the <see cref="Package.Downloads"/> column.
/// The <see cref="TableEntity.PartitionKey"/> is the <see cref="Package.Id"/> and
/// the <see cref="TableEntity.RowKey"/> is the <see cref="Package.Version"/>.
/// The <see cref="ITableEntity.PartitionKey"/> is the <see cref="Package.Id"/> and
/// the <see cref="ITableEntity.RowKey"/> is the <see cref="Package.Version"/>.
/// </summary>
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
Expand Down
21 changes: 8 additions & 13 deletions src/BaGetter.Azure/Table/PackageEntityExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -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<string[]>(entity.Authors),
Authors = JsonSerializer.Deserialize<string[]>(entity.Authors),
Description = entity.Description,
Downloads = entity.Downloads,
HasReadme = entity.HasReadme,
Expand All @@ -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<string[]>(entity.Tags),
Tags = JsonSerializer.Deserialize<string[]>(entity.Tags),
Dependencies = ParseDependencies(entity.Dependencies),
PackageTypes = ParsePackageTypes(entity.PackageTypes),
TargetFrameworks = ParseTargetFrameworks(entity.TargetFrameworks),
Expand All @@ -51,8 +49,7 @@ private static Uri ParseUri(string input)

private static List<PackageDependency> ParseDependencies(string input)
{
// TODO: Convert to System.Text.Json
return JsonConvert.DeserializeObject<List<DependencyModel>>(input)
return JsonSerializer.Deserialize<List<DependencyModel>>(input)
.Select(e => new PackageDependency
{
Id = e.Id,
Expand All @@ -64,8 +61,7 @@ private static List<PackageDependency> ParseDependencies(string input)

private static List<PackageType> ParsePackageTypes(string input)
{
// TODO: Convert to System.Text.Json
return JsonConvert.DeserializeObject<List<PackageTypeModel>>(input)
return JsonSerializer.Deserialize<List<PackageTypeModel>>(input)
.Select(e => new PackageType
{
Name = e.Name,
Expand All @@ -76,8 +72,7 @@ private static List<PackageType> ParsePackageTypes(string input)

private static List<TargetFramework> ParseTargetFrameworks(string targetFrameworks)
{
// TODO: Convert to System.Text.Json
return JsonConvert.DeserializeObject<List<string>>(targetFrameworks)
return JsonSerializer.Deserialize<List<string>>(targetFrameworks)
.Select(f => new TargetFramework { Moniker = f })
.ToList();
}
Expand Down
Loading

0 comments on commit 96c3041

Please sign in to comment.