diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b881ae643..7cd902c05 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -49,6 +49,7 @@ jobs: Hosting.RavenDB.Tests, Hosting.Redis.Extensions.Tests, Hosting.Rust.Tests, + Hosting.Solr.Tests, Hosting.SqlDatabaseProjects.Tests, Hosting.SqlServer.Extensions.Tests, Hosting.Sqlite.Tests, diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index a1c768323..a8d2c4b1f 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -131,6 +131,9 @@ + + + @@ -179,6 +182,7 @@ + @@ -228,6 +232,7 @@ + @@ -250,4 +255,4 @@ - + \ No newline at end of file diff --git a/examples/solr/CommunityToolkit.Aspire.Hosting.Solr.AppHost.csproj b/examples/solr/CommunityToolkit.Aspire.Hosting.Solr.AppHost.csproj new file mode 100644 index 000000000..11dc8afdc --- /dev/null +++ b/examples/solr/CommunityToolkit.Aspire.Hosting.Solr.AppHost.csproj @@ -0,0 +1,19 @@ + + + + + Exe + enable + enable + true + bfe6b134-1a06-4449-a146-ba3cdb0d02a6 + + + + + + + + + + diff --git a/examples/solr/Program.cs b/examples/solr/Program.cs new file mode 100644 index 000000000..9d4840050 --- /dev/null +++ b/examples/solr/Program.cs @@ -0,0 +1,14 @@ +var builder = DistributedApplication.CreateBuilder(args); + +// Add Solr resource with default core name "solr" +var solr = builder.AddSolr("solr"); + +// Add Solr resource with custom port and core name +var solrWithCustomPort = builder.AddSolr("solr-custom", port: 8984, coreName: "mycore"); + +// Reference the Solr resources in a project (example) +// var exampleProject = builder.AddProject() +// .WithReference(solr) +// .WithReference(solrWithCustomPort); + +builder.Build().Run(); diff --git a/src/CommunityToolkit.Aspire.Hosting.Solr/CommunityToolkit.Aspire.Hosting.Solr.csproj b/src/CommunityToolkit.Aspire.Hosting.Solr/CommunityToolkit.Aspire.Hosting.Solr.csproj new file mode 100644 index 000000000..a44287cd9 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Solr/CommunityToolkit.Aspire.Hosting.Solr.csproj @@ -0,0 +1,9 @@ + + + A .NET Aspire hosting integration for Apache Solr. + solr search hosting + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Solr/README.md b/src/CommunityToolkit.Aspire.Hosting.Solr/README.md new file mode 100644 index 000000000..e961c4390 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Solr/README.md @@ -0,0 +1,42 @@ +# CommunityToolkit.Aspire.Hosting.Solr + +This package provides a .NET Aspire hosting integration for [Apache Solr](https://solr.apache.org/), enabling you to add and configure a Solr container as part of your distributed application. + +## Getting Started + +### Install the package + +In your AppHost project, install the package using the following command: + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Hosting.Solr +``` + +## Usage Example + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +// Add Solr resource with default settings (port 8983, core "solr") +var solr = builder.AddSolr("solr"); + +// Add Solr with custom port +var solrWithCustomPort = builder.AddSolr("solr-custom", port: 8984); + +// Add Solr with custom core name +var solrWithCustomCore = builder.AddSolr("solr-core", coreName: "mycore"); + +// Add Solr with both custom port and core name +var solrCustom = builder.AddSolr("solr-full", port: 8985, coreName: "documents"); + +// Reference the Solr resource in a project +var exampleProject = builder.AddProject() + .WithReference(solr); + +// Initialize and run the application +builder.Build().Run(); +``` + +## Feedback & contributing + +https://github.com/dotnet/aspire diff --git a/src/CommunityToolkit.Aspire.Hosting.Solr/SolrBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Solr/SolrBuilderExtensions.cs new file mode 100644 index 000000000..b31e50bff --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Solr/SolrBuilderExtensions.cs @@ -0,0 +1,49 @@ +using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Hosting.Solr; +using Microsoft.Extensions.DependencyInjection; + +namespace Aspire.Hosting; + +/// +/// Extension methods for adding and configuring a Solr resource. +/// +public static class SolrBuilderExtensions +{ + /// + /// Adds an Apache Solr container resource to the distributed application. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The host port for Solr. + /// The name of the core to create. + /// A reference to the . + public static IResourceBuilder AddSolr(this IDistributedApplicationBuilder builder, [ResourceName] string name, int? port = null, string? coreName = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + coreName ??= "solr"; + + var resource = new SolrResource(name, coreName); + + var solrBuilder = builder.AddResource(resource) + .WithImage(SolrContainerImageTags.Image, SolrContainerImageTags.Tag) + .WithImageRegistry(SolrContainerImageTags.Registry) + .WithHttpEndpoint(targetPort: 8983, port: port, name: SolrResource.PrimaryEndpointName) + .WithArgs("solr-precreate", coreName); + + string healthCheckKey = $"{name}_check"; + var endpoint = solrBuilder.Resource.GetEndpoint(SolrResource.PrimaryEndpointName); + + builder.Services.AddHealthChecks() + .AddUrlGroup(options => + { + var uri = new Uri(endpoint.Url); + options.AddUri(new Uri(uri, $"solr/{coreName}/admin/ping"), setup => setup.ExpectHttpCode(200)); + }, healthCheckKey); + + solrBuilder.WithHealthCheck(healthCheckKey); + + return solrBuilder; + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Solr/SolrContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Solr/SolrContainerImageTags.cs new file mode 100644 index 000000000..2630e589f --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Solr/SolrContainerImageTags.cs @@ -0,0 +1,11 @@ +namespace CommunityToolkit.Aspire.Hosting.Solr; + +internal static class SolrContainerImageTags +{ + /// docker.io + public const string Registry = "docker.io"; + /// solr + public const string Image = "solr"; + /// 9.7 + public const string Tag = "9.7"; +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Solr/SolrResource.cs b/src/CommunityToolkit.Aspire.Hosting.Solr/SolrResource.cs new file mode 100644 index 000000000..714663a0c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Solr/SolrResource.cs @@ -0,0 +1,32 @@ +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents an Apache Solr container resource. +/// +/// The name of the resource. +/// The name of the Solr core. +public class SolrResource(string name, string coreName) : ContainerResource(name), IResourceWithConnectionString +{ + internal const string PrimaryEndpointName = "http"; + + private EndpointReference? _primaryEndpoint; + + /// + /// The Solr core name. + /// + public string CoreName { get; set; } = coreName; + + /// + /// Gets the primary endpoint for the Solr server. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + + /// + /// Gets the connection string expression for the Solr server. + /// + public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create( + $"http://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}/solr/{CoreName}"); + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Solr/api/CommunityToolkit.Aspire.Hosting.Solr.cs b/src/CommunityToolkit.Aspire.Hosting.Solr/api/CommunityToolkit.Aspire.Hosting.Solr.cs new file mode 100644 index 000000000..4ef5b4963 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Solr/api/CommunityToolkit.Aspire.Hosting.Solr.cs @@ -0,0 +1,25 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Aspire.Hosting +{ + public static partial class SolrBuilderExtensions + { + public static ApplicationModel.IResourceBuilder AddSolr(this IDistributedApplicationBuilder builder, string name, int? port = null, string? coreName = null) { throw null; } + } +} +namespace Aspire.Hosting.ApplicationModel +{ + public partial class SolrResource : ContainerResource, IResourceWithConnectionString, IResource, IManifestExpressionProvider, IValueProvider, IValueWithReferences + { + public SolrResource(string name, string coreName) : base(default!, default) { } + public string CoreName { get { throw null; } set { } } + public ReferenceExpression ConnectionStringExpression { get { throw null; } } + public EndpointReference PrimaryEndpoint { get { throw null; } } + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Solr.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Solr.Tests/AppHostTests.cs new file mode 100644 index 000000000..affe52808 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Solr.Tests/AppHostTests.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Aspire.Components.Common.Tests; +using CommunityToolkit.Aspire.Testing; + +namespace CommunityToolkit.Aspire.Hosting.Solr.Tests; + +[RequiresDocker] +public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> +{ + [Fact] + public async Task SolrResourceStartsAndRespondsOk() + { + var resourceName = "solr"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5)); + var httpClient = fixture.CreateHttpClient(resourceName); + + var response = await httpClient.GetAsync("/solr/"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task SolrResourceWithCustomPortStartsAndRespondsOk() + { + var resourceName = "solr-custom"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5)); + var httpClient = fixture.CreateHttpClient(resourceName); + + var response = await httpClient.GetAsync("/solr/"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task SolrCoreIsHealthy() + { + var resourceName = "solr"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5)); + var httpClient = fixture.CreateHttpClient(resourceName); + + // Test that the specific core admin ping endpoint works + var response = await httpClient.GetAsync("/solr/solr/admin/ping"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task SolrCustomCoreIsHealthy() + { + var resourceName = "solr-custom"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5)); + var httpClient = fixture.CreateHttpClient(resourceName); + + // Test that the custom core admin ping endpoint works + var response = await httpClient.GetAsync("/solr/mycore/admin/ping"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Solr.Tests/CommunityToolkit.Aspire.Hosting.Solr.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Solr.Tests/CommunityToolkit.Aspire.Hosting.Solr.Tests.csproj new file mode 100644 index 000000000..034e6245f --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Solr.Tests/CommunityToolkit.Aspire.Hosting.Solr.Tests.csproj @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.Solr.Tests/SolrResourceTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Solr.Tests/SolrResourceTests.cs new file mode 100644 index 000000000..6e79a0d6b --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Solr.Tests/SolrResourceTests.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace CommunityToolkit.Aspire.Hosting.Solr.Tests; + +public class SolrResourceTests +{ + [Fact] + public void AddSolr_CreatesCorrectResource() + { + var builder = DistributedApplication.CreateBuilder(); + var solr = builder.AddSolr("solr"); + + Assert.NotNull(solr.Resource); + Assert.Equal("solr", solr.Resource.Name); + Assert.IsType(solr.Resource); + Assert.Equal("solr", solr.Resource.CoreName); + } + + [Fact] + public void AddSolr_WithCustomCoreName_ConfiguresCorrectly() + { + var builder = DistributedApplication.CreateBuilder(); + var solr = builder.AddSolr("solr", coreName: "mycustomcore"); + + Assert.NotNull(solr.Resource); + Assert.Equal("solr", solr.Resource.Name); + Assert.Equal("mycustomcore", solr.Resource.CoreName); + } + + [Fact] + public void AddSolr_WithPort_ConfiguresCorrectly() + { + var builder = DistributedApplication.CreateBuilder(); + var solr = builder.AddSolr("solr", port: 8984); + + Assert.NotNull(solr.Resource); + Assert.Equal("solr", solr.Resource.Name); + Assert.Equal("solr", solr.Resource.CoreName); + } + + [Fact] + public void AddSolr_WithPortAndCoreName_ConfiguresCorrectly() + { + var builder = DistributedApplication.CreateBuilder(); + var solr = builder.AddSolr("solr", port: 8984, coreName: "testcore"); + + Assert.NotNull(solr.Resource); + Assert.Equal("solr", solr.Resource.Name); + Assert.Equal("testcore", solr.Resource.CoreName); + } + + [Fact] + public void SolrResource_HasCorrectConnectionString() + { + var builder = DistributedApplication.CreateBuilder(); + var solr = builder.AddSolr("solr", coreName: "mycore"); + + Assert.NotNull(solr.Resource.ConnectionStringExpression); + // Connection string should contain the core name + var connectionString = solr.Resource.ConnectionStringExpression.ValueExpression; + Assert.Contains("mycore", connectionString); + Assert.Contains("/solr/mycore", connectionString); + } + + [Fact] + public void SolrResource_HasHealthCheck() + { + var builder = DistributedApplication.CreateBuilder(); + var solr = builder.AddSolr("solr", coreName: "testcore"); + + using var app = builder.Build(); + var healthChecks = app.Services.GetRequiredService(); + Assert.NotNull(healthChecks); + } + + [Fact] + public void SolrResource_DefaultCoreName_IsSolr() + { + var builder = DistributedApplication.CreateBuilder(); + var solr = builder.AddSolr("solr"); + + Assert.Equal("solr", solr.Resource.CoreName); + } +}