Skip to content

Commit

Permalink
Making it more flexible to work with extensions, but also marking the…
Browse files Browse the repository at this point in the history
… feature as experimental
  • Loading branch information
aaronpowell committed Jan 30, 2025
1 parent 3b3faa4 commit ca34e67
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 26 deletions.
15 changes: 13 additions & 2 deletions src/CommunityToolkit.Aspire.Hosting.Sqlite/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
#nullable enable
Aspire.Hosting.ApplicationModel.ExtensionMetadata
Aspire.Hosting.ApplicationModel.ExtensionMetadata.Extension.get -> string!
Aspire.Hosting.ApplicationModel.ExtensionMetadata.Extension.init -> void
Aspire.Hosting.ApplicationModel.ExtensionMetadata.ExtensionFolder.get -> string?
Aspire.Hosting.ApplicationModel.ExtensionMetadata.ExtensionFolder.init -> void
Aspire.Hosting.ApplicationModel.ExtensionMetadata.ExtensionMetadata(string! Extension, string? PackageName, bool IsNuGetPackage, string? ExtensionFolder) -> void
Aspire.Hosting.ApplicationModel.ExtensionMetadata.IsNuGetPackage.get -> bool
Aspire.Hosting.ApplicationModel.ExtensionMetadata.IsNuGetPackage.init -> void
Aspire.Hosting.ApplicationModel.ExtensionMetadata.PackageName.get -> string?
Aspire.Hosting.ApplicationModel.ExtensionMetadata.PackageName.init -> void
Aspire.Hosting.ApplicationModel.SqliteResource
Aspire.Hosting.ApplicationModel.SqliteResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression!
Aspire.Hosting.ApplicationModel.SqliteResource.Extensions.get -> System.Collections.Generic.IReadOnlyCollection<string!>!
Aspire.Hosting.ApplicationModel.SqliteResource.Extensions.get -> System.Collections.Generic.IReadOnlyCollection<Aspire.Hosting.ApplicationModel.ExtensionMetadata!>!
Aspire.Hosting.ApplicationModel.SqliteResource.SqliteResource(string! name, string! databasePath, string! databaseFileName) -> void
Aspire.Hosting.ApplicationModel.SqliteWebResource
Aspire.Hosting.ApplicationModel.SqliteWebResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression!
Aspire.Hosting.ApplicationModel.SqliteWebResource.PrimaryEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference!
Aspire.Hosting.ApplicationModel.SqliteWebResource.SqliteWebResource(string! name) -> void
Aspire.Hosting.SqliteResourceBuilderExtensions
static Aspire.Hosting.SqliteResourceBuilderExtensions.AddSqlite(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string? databasePath = null, string? databaseFileName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqliteResource!>!
static Aspire.Hosting.SqliteResourceBuilderExtensions.WithExtension(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqliteResource!>! builder, string! extension) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqliteResource!>!
static Aspire.Hosting.SqliteResourceBuilderExtensions.WithLocalExtension(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqliteResource!>! builder, string! extension, string! extensionPath) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqliteResource!>!
static Aspire.Hosting.SqliteResourceBuilderExtensions.WithNuGetExtension(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqliteResource!>! builder, string! extension, string? packageName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqliteResource!>!
static Aspire.Hosting.SqliteResourceBuilderExtensions.WithSqliteWeb(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqliteResource!>! builder, string? containerName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqliteResource!>!
15 changes: 12 additions & 3 deletions src/CommunityToolkit.Aspire.Hosting.Sqlite/SqliteResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,24 @@ public class SqliteResource(string name, string databasePath, string databaseFil
/// <inheritdoc/>
public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"Data Source={DatabaseFilePath};Cache=Shared;Mode=ReadWriteCreate;Extensions={JsonSerializer.Serialize(Extensions)}");

private readonly List<string> extensions = [];
private readonly List<ExtensionMetadata> extensions = [];

/// <summary>
/// Gets the extensions to be loaded into the database.
/// </summary>
/// <remarks>
/// Extensions are not loaded by the hosting integration, the information is provided for the client to load the extensions.
/// </remarks>
public IReadOnlyCollection<string> Extensions => extensions;
public IReadOnlyCollection<ExtensionMetadata> Extensions => extensions;

internal void AddExtension(string extension) => extensions.Add(extension);
internal void AddExtension(ExtensionMetadata extension) => extensions.Add(extension);
}

/// <summary>
/// Represents metadata for an extension to be loaded into a database.
/// </summary>
/// <param name="Extension">The name of the extension binary, eg: vec0</param>
/// <param name="PackageName">The name of the NuGet package. Only required if <paramref name="IsNuGetPackage"/> is <see langword="true" />.</param>
/// <param name="IsNuGetPackage">Indicates if the extension will be loaded from a NuGet package.</param>
/// <param name="ExtensionFolder">The folder for the extension. Only required if <paramref name="IsNuGetPackage"/> is <see langword="false" />.</param>
public record ExtensionMetadata(string Extension, string? PackageName, bool IsNuGetPackage, string? ExtensionFolder);
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Aspire.Hosting.ApplicationModel;
using System.Diagnostics.CodeAnalysis;

namespace Aspire.Hosting;

Expand Down Expand Up @@ -87,18 +88,48 @@ public static IResourceBuilder<SqliteResource> WithSqliteWeb(this IResourceBuild
}

/// <summary>
/// Adds an extension to the Sqlite resource.
/// Adds an extension to the Sqlite resource that will be loaded from a NuGet package.
/// </summary>
/// <param name="builder">The resource builder.</param>
/// <param name="extension">The extension to add.</param>
/// <param name="extension">The name of the extension file with to add, eg: vec0, without file extension.</param>
/// <param name="packageName">The name of the NuGet package. If this is set to null, the value of <paramref name="extension"/> is used.</param>
/// <returns>The resource builder.</returns>
/// <remarks>Extensions are not loaded by the hosting integration, the information is provided for the client to load the extensions.</remarks>
public static IResourceBuilder<SqliteResource> WithExtension(this IResourceBuilder<SqliteResource> builder, string extension)
/// <remarks>
/// Extensions are not loaded by the hosting integration, the information is provided for the client to load the extensions.
///
/// This extension is experimental while the final design of extension loading is decided.
/// </remarks>
[Experimental("CTASPIRE002", UrlFormat = "https://aka.ms/communitytoolkit/aspire/diagnostics#{0}")]
public static IResourceBuilder<SqliteResource> WithNuGetExtension(this IResourceBuilder<SqliteResource> builder, string extension, string? packageName = null)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
ArgumentNullException.ThrowIfNull(extension, nameof(extension));
ArgumentException.ThrowIfNullOrEmpty(extension, nameof(extension));

builder.Resource.AddExtension(extension);
builder.Resource.AddExtension(new(extension, packageName ?? extension, IsNuGetPackage: true, ExtensionFolder: null));

return builder;
}

/// <summary>
/// Adds an extension to the Sqlite resource that will be loaded from a local path.
/// </summary>
/// <param name="builder">The resource builder.</param>
/// <param name="extension">The name of the extension file with to add, eg: vec0, without file extension.</param>
/// <param name="extensionPath">The path to the extension file.</param>
/// <returns>The resource builder.</returns>
/// <remarks>
/// Extensions are not loaded by the hosting integration, the information is provided for the client to load the extensions.
///
/// This extension is experimental while the final design of extension loading is decided.
/// </remarks> find . -type f -name "*.orig" -delete
[Experimental("CTASPIRE002", UrlFormat = "https://aka.ms/communitytoolkit/aspire/diagnostics#{0}")]
public static IResourceBuilder<SqliteResource> WithLocalExtension(this IResourceBuilder<SqliteResource> builder, string extension, string extensionPath)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
ArgumentException.ThrowIfNullOrEmpty(extension, nameof(extension));
ArgumentException.ThrowIfNullOrEmpty(extensionPath, nameof(extensionPath));

builder.Resource.AddExtension(new(extension, PackageName: null, IsNuGetPackage: false, extensionPath));

return builder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ private static void AddSqliteClient(
var cbs = new DbConnectionStringBuilder { ConnectionString = settings.ConnectionString };
if (cbs.TryGetValue("Extensions", out var extensions))
{
settings.Extensions = JsonSerializer.Deserialize<IEnumerable<string>>((string)extensions) ?? [];
settings.Extensions = JsonSerializer.Deserialize<IEnumerable<ExtensionMetadata>>((string)extensions) ?? [];
}
}

Expand Down Expand Up @@ -124,16 +124,33 @@ SqliteConnection CreateConnection(IServiceProvider sp, object? key)

foreach (var extension in settings.Extensions)
{
EnsureLoadable(extension, extension);
connection.LoadExtension(extension);
if (extension.IsNuGetPackage)
{
if (string.IsNullOrEmpty(extension.PackageName))
{
throw new InvalidOperationException("PackageName is required when loading an extension from a NuGet package.");
}

EnsureLoadableFromNuGet(extension.Extension, extension.PackageName);
}
else
{
if (string.IsNullOrEmpty(extension.ExtensionFolder))
{
throw new InvalidOperationException("ExtensionFolder is required when loading an extension from a folder.");
}

EnsureLoadableFromLocalPath(extension.Extension, extension.ExtensionFolder);
}
connection.LoadExtension(extension.Extension);
}

return connection;
}
}

// Adapted from https://github.com/dotnet/docs/blob/dbbeda13bf016a6ff76b0baab1488c927a64ff24/samples/snippets/standard/data/sqlite/ExtensionsSample/Program.cs#L40
internal static void EnsureLoadable(string package, string library)
internal static void EnsureLoadableFromNuGet(string package, string library)
{
var runtimeLibrary = DependencyContext.Default?.RuntimeLibraries.FirstOrDefault(l => l.Name == package);
if (runtimeLibrary is null)
Expand Down Expand Up @@ -223,4 +240,36 @@ internal static void EnsureLoadable(string package, string library)
Environment.SetEnvironmentVariable(pathVariableName, string.Join(Path.PathSeparator, path));
}
}

internal static void EnsureLoadableFromLocalPath(string library, string assetDirectory)
{
string sharedLibraryExtension;
string pathVariableName = "PATH";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
sharedLibraryExtension = ".dll";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
sharedLibraryExtension = ".so";
pathVariableName = "LD_LIBRARY_PATH";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
sharedLibraryExtension = ".dylib";
pathVariableName = "DYLD_LIBRARY_PATH";
}
else
{
throw new NotSupportedException("Unsupported OS platform");
}

if (File.Exists(Path.Combine(assetDirectory, library + sharedLibraryExtension)))
{
var path = new HashSet<string>(Environment.GetEnvironmentVariable(pathVariableName)!.Split(Path.PathSeparator));

if (assetDirectory is not null && path.Add(assetDirectory))
Environment.SetEnvironmentVariable(pathVariableName, string.Join(Path.PathSeparator, path));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
#nullable enable
Microsoft.Extensions.Hosting.AspireSqliteExtensions
Microsoft.Extensions.Hosting.ExtensionMetadata
Microsoft.Extensions.Hosting.ExtensionMetadata.Extension.get -> string!
Microsoft.Extensions.Hosting.ExtensionMetadata.Extension.init -> void
Microsoft.Extensions.Hosting.ExtensionMetadata.ExtensionFolder.get -> string?
Microsoft.Extensions.Hosting.ExtensionMetadata.ExtensionFolder.init -> void
Microsoft.Extensions.Hosting.ExtensionMetadata.ExtensionMetadata(string! Extension, string? PackageName, bool IsNuGetPackage, string? ExtensionFolder) -> void
Microsoft.Extensions.Hosting.ExtensionMetadata.IsNuGetPackage.get -> bool
Microsoft.Extensions.Hosting.ExtensionMetadata.IsNuGetPackage.init -> void
Microsoft.Extensions.Hosting.ExtensionMetadata.PackageName.get -> string?
Microsoft.Extensions.Hosting.ExtensionMetadata.PackageName.init -> void
Microsoft.Extensions.Hosting.SqliteConnectionSettings
Microsoft.Extensions.Hosting.SqliteConnectionSettings.ConnectionString.get -> string?
Microsoft.Extensions.Hosting.SqliteConnectionSettings.ConnectionString.set -> void
Microsoft.Extensions.Hosting.SqliteConnectionSettings.DisableHealthChecks.get -> bool
Microsoft.Extensions.Hosting.SqliteConnectionSettings.DisableHealthChecks.set -> void
Microsoft.Extensions.Hosting.SqliteConnectionSettings.Extensions.get -> System.Collections.Generic.IEnumerable<string!>!
Microsoft.Extensions.Hosting.SqliteConnectionSettings.Extensions.get -> System.Collections.Generic.IEnumerable<Microsoft.Extensions.Hosting.ExtensionMetadata!>!
Microsoft.Extensions.Hosting.SqliteConnectionSettings.Extensions.set -> void
Microsoft.Extensions.Hosting.SqliteConnectionSettings.SqliteConnectionSettings() -> void
static Microsoft.Extensions.Hosting.AspireSqliteExtensions.AddKeyedSqliteConnection(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! name, System.Action<Microsoft.Extensions.Hosting.SqliteConnectionSettings!>? configureSettings = null) -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,14 @@ public sealed class SqliteConnectionSettings
/// <summary>
/// Extensions to be loaded into the database.
/// </summary>
public IEnumerable<string> Extensions { get; set; } = [];
public IEnumerable<ExtensionMetadata> Extensions { get; set; } = [];
}

/// <summary>
/// Represents metadata for an extension to be loaded into a database.
/// </summary>
/// <param name="Extension">The name of the extension binary, eg: vec0</param>
/// <param name="PackageName">The name of the NuGet package. Only required if <paramref name="IsNuGetPackage"/> is <see langword="true" />.</param>
/// <param name="IsNuGetPackage">Indicates if the extension will be loaded from a NuGet package.</param>
/// <param name="ExtensionFolder">The folder for the extension. Only required if <paramref name="IsNuGetPackage"/> is <see langword="false" />.</param>
public record ExtensionMetadata(string Extension, string? PackageName, bool IsNuGetPackage, string? ExtensionFolder);
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Aspire.Hosting;

namespace CommunityToolkit.Aspire.Hosting.Sqlite;

#pragma warning disable CTASPIRE002
public class AddSqliteTests
{
[Fact]
Expand Down Expand Up @@ -136,22 +136,32 @@ public async Task SqliteWebResourceConfigured()
}

[Fact]
public void ResourceWithExtension()
public void ResourceWithExtensionFromNuGet()
{
var builder = DistributedApplication.CreateBuilder();
var sqlite = builder.AddSqlite("sqlite")
.WithNuGetExtension("FTS5");

Assert.Single(sqlite.Resource.Extensions, static e => e.Extension == "FTS5" && e.PackageName == "FTS5" && e.IsNuGetPackage && e.ExtensionFolder is null);
}

[Fact]
public void ResourceWithExtensionFromLocal()
{
var builder = DistributedApplication.CreateBuilder();
var sqlite = builder.AddSqlite("sqlite")
.WithExtension("FTS5");
.WithLocalExtension("FTS5", "/path/to/extension");

Assert.Contains("FTS5", sqlite.Resource.Extensions);
Assert.Single(sqlite.Resource.Extensions, static e => e.Extension == "FTS5" && e.PackageName is null && !e.IsNuGetPackage && e.ExtensionFolder == "/path/to/extension");
}

[Fact]
public async Task ConnectionStringContainsExtensions()
{
var builder = DistributedApplication.CreateBuilder();
var sqlite = builder.AddSqlite("sqlite")
.WithExtension("FTS5")
.WithExtension("mod_spatialite");
.WithNuGetExtension("FTS5")
.WithNuGetExtension("mod_spatialite");

var connectionString = await sqlite.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,23 +123,23 @@ public void ExtensionsSetViaConnectionString(bool useKeyed)
{
var builder = Host.CreateEmptyApplicationBuilder(null);
builder.Configuration.AddInMemoryCollection([
new KeyValuePair<string, string?>("ConnectionStrings:sqlite", "Data Source=:memory:;Extensions=[\"mod_spatialite\"]")
new KeyValuePair<string, string?>("ConnectionStrings:sqlite", "Data Source=:memory:;Extensions=[{\"Extension\":\"mod_spatialite\",\"PackageName\":\"mod_spatialite\",\"IsNuGetPackage\":true,\"ExtensionFolder\":null}]")
]);

if (useKeyed)
{
builder.AddKeyedSqliteConnection("sqlite", settings =>
{
Assert.NotEmpty(settings.Extensions);
Assert.Contains("mod_spatialite", settings.Extensions);
Assert.Single(settings.Extensions, e => e.Extension == "mod_spatialite" && e.PackageName == "mod_spatialite" && e.IsNuGetPackage && e.ExtensionFolder is null);
});
}
else
{
builder.AddSqliteConnection("sqlite", settings =>
{
Assert.NotEmpty(settings.Extensions);
Assert.Contains("mod_spatialite", settings.Extensions);
Assert.Single(settings.Extensions, e => e.Extension == "mod_spatialite" && e.PackageName == "mod_spatialite" && e.IsNuGetPackage && e.ExtensionFolder is null);
});
}
}
Expand Down

0 comments on commit ca34e67

Please sign in to comment.