diff --git a/.build/dependencies.props b/.build/dependencies.props index f3c8dda3fa..4c2e671329 100644 --- a/.build/dependencies.props +++ b/.build/dependencies.props @@ -1,4 +1,4 @@ - [2.1.0, 3.0.0) 1.0.9 - 2.1.1 - 2.1.34 - 6.0.0 - 2.1.1 - 3.1.32 + 2.3.0 + 8.0.19 2.9.8 2.6.1 $(MicrosoftCodeAnalysisCSharpPackageVersion) @@ -71,7 +68,6 @@ 2.7.8 1.4.2 0.4.1.1 - 8.0.0 4.5.5 4.3.4 4.3.0 @@ -80,7 +76,6 @@ 4.3.0 4.3.0 5.0.0 - 8.0.0 6.0.10 4.3.1 6.1.0 diff --git a/Directory.Build.targets b/Directory.Build.targets index 18700d0be7..efbd8089f3 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -40,6 +40,7 @@ + $(DefineConstants);FEATURE_ASPNETCORE_TESTHOST $(DefineConstants);FEATURE_UTF8_TOUTF16 @@ -56,7 +57,6 @@ - $(DefineConstants);FEATURE_ASPNETCORE_ENDPOINT_CONFIG $(DefineConstants);FEATURE_READONLYSET diff --git a/src/Lucene.Net.Replicator/Http/ReplicationService.cs b/src/Lucene.Net.Replicator/Http/ReplicationService.cs index ae2426a8f4..ccdaba7e92 100644 --- a/src/Lucene.Net.Replicator/Http/ReplicationService.cs +++ b/src/Lucene.Net.Replicator/Http/ReplicationService.cs @@ -7,6 +7,9 @@ using System.Collections.Generic; using System.IO; using System.Net; +using System.Threading; +using System.Threading.Tasks; + namespace Lucene.Net.Replicator.Http { @@ -118,29 +121,23 @@ private static string ExtractRequestParam(IReplicationRequest request, string pa return param; } - // LUCENENET specific - copy method not used - - /// - /// Executes the replication task. - /// - /// required parameters are missing - public virtual void Perform(IReplicationRequest request, IReplicationResponse response) + // method to avoid code duplication in sync and async Perform methods + private async Task ExecuteReplicationAsync( + IReplicationRequest request, + IReplicationResponse response, + Func copyStreamFunc, + Func writeTokenFunc, + Func flushFunc) { string[] pathElements = GetPathElements(request); if (pathElements.Length != 2) - { throw ServletException.Create("invalid path, must contain shard ID and action, e.g. */s1/update"); - } if (!Enum.TryParse(pathElements[ACTION_IDX], true, out ReplicationAction action)) - { throw ServletException.Create("Unsupported action provided: " + pathElements[ACTION_IDX]); - } if (!replicators.TryGetValue(pathElements[SHARD_IDX], out IReplicator replicator)) - { throw ServletException.Create("unrecognized shard ID " + pathElements[SHARD_IDX]); - } // SOLR-8933 Don't close this stream. try @@ -148,32 +145,30 @@ public virtual void Perform(IReplicationRequest request, IReplicationResponse re switch (action) { case ReplicationAction.OBTAIN: - string sessionId = ExtractRequestParam(request, REPLICATE_SESSION_ID_PARAM); - string fileName = ExtractRequestParam(request, REPLICATE_FILENAME_PARAM); - string source = ExtractRequestParam(request, REPLICATE_SOURCE_PARAM); - using (Stream stream = replicator.ObtainFile(sessionId, source, fileName)) - stream.CopyTo(response.Body); - break; + { + string sessionId = ExtractRequestParam(request, REPLICATE_SESSION_ID_PARAM); + string fileName = ExtractRequestParam(request, REPLICATE_FILENAME_PARAM); + string source = ExtractRequestParam(request, REPLICATE_SOURCE_PARAM); - case ReplicationAction.RELEASE: - replicator.Release(ExtractRequestParam(request, REPLICATE_SESSION_ID_PARAM)); - break; + using (Stream stream = replicator.ObtainFile(sessionId, source, fileName)) + await copyStreamFunc(stream); + break; + } - case ReplicationAction.UPDATE: - string currentVersion = request.QueryParam(REPLICATE_VERSION_PARAM); - SessionToken token = replicator.CheckForUpdate(currentVersion); - if (token is null) + case ReplicationAction.RELEASE: { - response.Body.Write(new byte[] { 0 }, 0, 1); // marker for null token + replicator.Release(ExtractRequestParam(request, REPLICATE_SESSION_ID_PARAM)); + break; } - else + + case ReplicationAction.UPDATE: { - response.Body.Write(new byte[] { 1 }, 0, 1); - token.Serialize(new DataOutputStream(response.Body)); + string currentVersion = request.QueryParam(REPLICATE_VERSION_PARAM); + SessionToken token = replicator.CheckForUpdate(currentVersion); + await writeTokenFunc(token); + break; } - break; - // LUCENENET specific: default: if (Debugging.AssertsEnabled) Debugging.Assert(false, "Invalid ReplicationAction specified"); break; @@ -185,8 +180,70 @@ public virtual void Perform(IReplicationRequest request, IReplicationResponse re } finally { - response.Flush(); + await flushFunc(); } } + + // LUCENENET specific - copy method not used + + /// + /// Executes the replication task. + /// + /// required parameters are missing + public virtual void Perform(IReplicationRequest request, IReplicationResponse response) + { + ExecuteReplicationAsync( + request, + response, + stream => { stream.CopyTo(response.Body); return Task.CompletedTask; }, + token => + { + if (token == null) + { + response.Body.Write(new byte[] { 0 }, 0, 1); + } + else + { + response.Body.Write(new byte[] { 1 }, 0, 1); + token.Serialize(new DataOutputStream(response.Body)); + } + return Task.CompletedTask; + }, + () => { response.Body.Flush(); return Task.CompletedTask; } + ).ConfigureAwait(false).GetAwaiter().GetResult(); // keep sync behavior + } + + + /// + /// Executes the replication task asynchronously. + /// + /// The replication request containing action and parameters. + /// The replication response used to send data back to the client. + /// A to observe while performing the replication. + /// Thrown when required parameters are missing or invalid. + public virtual Task PerformAsync( + IReplicationRequest request, + IReplicationResponse response, + CancellationToken cancellationToken = default) + { + return ExecuteReplicationAsync( + request, + response, + stream => stream.CopyToAsync(response.Body, 81920, cancellationToken), + async token => + { + if (token == null) + { + await response.Body.WriteAsync(new byte[] { 0 }, 0, 1, cancellationToken); + } + else + { + await response.Body.WriteAsync(new byte[] { 1 }, 0, 1, cancellationToken); + await token.SerializeAsync(response.Body, cancellationToken); + } + }, + () => response.Body.FlushAsync(cancellationToken) + ); + } } } diff --git a/src/Lucene.Net.Replicator/SessionToken.cs b/src/Lucene.Net.Replicator/SessionToken.cs index fd17649d73..d8024f4d2e 100644 --- a/src/Lucene.Net.Replicator/SessionToken.cs +++ b/src/Lucene.Net.Replicator/SessionToken.cs @@ -1,7 +1,11 @@ using J2N.IO; using System.Collections.Generic; +using System; using System.IO; using JCG = J2N.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Lucene.Net.Support.IO; namespace Lucene.Net.Replicator { @@ -112,6 +116,37 @@ public void Serialize(DataOutputStream writer) } } + /// + /// Asynchronously serializes the token's properties, including ID, version, and source files, + /// to the provided for transmission or storage. + /// + /// The to write the token data to. + /// A cancellation token to observe while writing and flushing the stream. + /// A task representing the asynchronous serialization operation. + internal async Task SerializeAsync(Stream output, CancellationToken cancellationToken = default) + { + if (output is null) + throw new ArgumentNullException(nameof(output)); + + await output.WriteUTFAsync(Id, cancellationToken).ConfigureAwait(false); + await output.WriteUTFAsync(Version, cancellationToken).ConfigureAwait(false); + await output.WriteInt32BigEndianAsync(SourceFiles.Count, cancellationToken).ConfigureAwait(false); + + foreach (var pair in SourceFiles) + { + await output.WriteUTFAsync(pair.Key, cancellationToken).ConfigureAwait(false); + await output.WriteInt32BigEndianAsync(pair.Value.Count, cancellationToken).ConfigureAwait(false); + + foreach (var file in pair.Value) + { + await output.WriteUTFAsync(file.FileName, cancellationToken).ConfigureAwait(false); + await output.WriteInt64BigEndianAsync(file.Length, cancellationToken).ConfigureAwait(false); + } + } + + await output.FlushAsync(cancellationToken).ConfigureAwait(false); + } + public override string ToString() { return string.Format("id={0} version={1} files={2}", Id, Version, SourceFiles); diff --git a/src/Lucene.Net.Replicator/Support/Http/Abstractions/IReplicationResponse.cs b/src/Lucene.Net.Replicator/Support/Http/Abstractions/IReplicationResponse.cs index e55b37996b..2d035d34af 100644 --- a/src/Lucene.Net.Replicator/Support/Http/Abstractions/IReplicationResponse.cs +++ b/src/Lucene.Net.Replicator/Support/Http/Abstractions/IReplicationResponse.cs @@ -37,10 +37,5 @@ public interface IReplicationResponse /// The response content. /// Stream Body { get; } - - /// - /// Flushes the reponse to the underlying response stream. - /// - void Flush(); } } diff --git a/src/Lucene.Net.Replicator/Support/Http/Abstractions/IReplicationService.cs b/src/Lucene.Net.Replicator/Support/Http/Abstractions/IReplicationService.cs index c1e67e22b8..78ca9bf565 100644 --- a/src/Lucene.Net.Replicator/Support/Http/Abstractions/IReplicationService.cs +++ b/src/Lucene.Net.Replicator/Support/Http/Abstractions/IReplicationService.cs @@ -1,4 +1,7 @@ using System; +using System.Threading; +using System.Threading.Tasks; + namespace Lucene.Net.Replicator.Http.Abstractions { @@ -29,5 +32,15 @@ public interface IReplicationService /// /// required parameters are missing void Perform(IReplicationRequest request, IReplicationResponse response); + + /// + /// Executes the replication task asynchronously. + /// + /// The replication request. + /// The replication response. + /// Optional cancellation token. + /// A task representing the asynchronous operation. + Task PerformAsync(IReplicationRequest request, IReplicationResponse response, CancellationToken cancellationToken = default); + } } diff --git a/src/Lucene.Net.Tests.Replicator/Http/HttpReplicatorTest.cs b/src/Lucene.Net.Tests.Replicator/Http/HttpReplicatorTest.cs index a5106e1979..3466007883 100644 --- a/src/Lucene.Net.Tests.Replicator/Http/HttpReplicatorTest.cs +++ b/src/Lucene.Net.Tests.Replicator/Http/HttpReplicatorTest.cs @@ -2,7 +2,6 @@ using Lucene.Net.Index; using Lucene.Net.Support; using Lucene.Net.Util; -using Microsoft.AspNetCore.TestHost; using NUnit.Framework; using System; using System.Collections.Generic; @@ -10,6 +9,13 @@ using System.IO; using Directory = Lucene.Net.Store.Directory; +#if FEATURE_ASPNETCORE_TESTHOST +using Microsoft.AspNetCore.TestHost; +#else +using Lucene.Net.Replicator.Net; +#endif + + namespace Lucene.Net.Replicator.Http { /* @@ -29,6 +35,14 @@ namespace Lucene.Net.Replicator.Http * limitations under the License. */ + // Technically, the ConfigOption is only supported by ASP.NET Core + // so we just ignore the other option when running on HttpListener. + [TestFixture(IOOption.Synchronous, ConfigOption.StartupClass)] + [TestFixture(IOOption.Asynchronous, ConfigOption.StartupClass)] +#if FEATURE_ASPNETCORE_TESTHOST + [TestFixture(IOOption.Synchronous, ConfigOption.Middleware)] + [TestFixture(IOOption.Asynchronous, ConfigOption.Middleware)] +#endif public class HttpReplicatorTest : ReplicatorTestCase { private DirectoryInfo clientWorkDir; @@ -45,16 +59,31 @@ public class HttpReplicatorTest : ReplicatorTestCase private MockErrorConfig mockErrorConfig; - private void StartServer() + private readonly bool useSynchronousIO; + private readonly bool useStartupClass; + + public enum IOOption { - ReplicationService service = new ReplicationService(new Dictionary { { "s1", serverReplicator } }); + Synchronous, + Asynchronous, + } -#if FEATURE_ASPNETCORE_ENDPOINT_CONFIG - server = NewHttpServer(service, mockErrorConfig); // Call like this to use ReplicationServerMiddleware on the specific path /replicate/{shard?}/{action?}, but allow other paths to be served -#else - server = NewHttpServer(service, mockErrorConfig); // Call like this to use ReplicationServlet as a Startup Class -#endif + public enum ConfigOption + { + StartupClass, + Middleware + } + + public HttpReplicatorTest(IOOption ioOption, ConfigOption configOption) + { + this.useSynchronousIO = ioOption == IOOption.Synchronous; + this.useStartupClass = configOption == ConfigOption.StartupClass; + } + private void StartServer() + { + ReplicationService service = new ReplicationService(new Dictionary { { "s1", serverReplicator } }); + server = NewHttpServer(service, mockErrorConfig, useSynchronousIO, useStartupClass); port = ServerPort(server); host = ServerHost(server); } @@ -144,7 +173,7 @@ public void TestServerErrors() mockErrorConfig.RespondWithError = false; client.UpdateNow(); // now it should work ReopenReader(); - assertEquals(5, J2N.Numerics.Int32.Parse(reader.IndexCommit.UserData["ID"], 16)); + assertEquals(5, int.Parse(reader.IndexCommit.UserData["ID"], NumberStyles.HexNumber)); client.Dispose(); } diff --git a/src/Lucene.Net.Tests.Replicator/Http/ReplicationServlet.cs b/src/Lucene.Net.Tests.Replicator/Http/ReplicationServlet.cs index 8146470a66..0aa4dc33a1 100644 --- a/src/Lucene.Net.Tests.Replicator/Http/ReplicationServlet.cs +++ b/src/Lucene.Net.Tests.Replicator/Http/ReplicationServlet.cs @@ -1,15 +1,12 @@ +#if FEATURE_ASPNETCORE_TESTHOST using Lucene.Net.Replicator.AspNetCore; using Lucene.Net.Replicator.Http.Abstractions; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http.Features; -using System; -using System.Threading.Tasks; - -#if FEATURE_ASPNETCORE_ENDPOINT_CONFIG using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -#endif +using System; +using System.Threading.Tasks; namespace Lucene.Net.Replicator.Http { @@ -31,7 +28,7 @@ namespace Lucene.Net.Replicator.Http */ // ********************** Option 1: Use a Startup Class ******************************************** - // The startup class must define all middleware (app.Use) before the terminating endpoint (app.Run) + // The startup class must define all middleware (app.Use) before the terminating endpoint (app.Run). public class ReplicationServlet { @@ -50,16 +47,11 @@ public void Configure(IApplicationBuilder app, IReplicationService service, Repl app.Run(async (context) => { - // LUCENENET: This is to allow synchronous IO to happen for these requests. - // LUCENENET TODO: Allow async operations from Replicator. - var syncIoFeature = context.Features.Get(); - if (syncIoFeature != null) - { - syncIoFeature.AllowSynchronousIO = true; - } + // LUCENENET: Although the async/await pattern doesn't exist in Java Lucene, this is the recommended + // approach for modern .NET development. + await service.PerformAsync(context.Request, context.Response, context.RequestAborted); - await Task.Yield(); - service.Perform(context.Request, context.Response); + // This is a terminating endpoint. Do not call the next delegate/middleware in the pipeline. }); } } @@ -68,7 +60,6 @@ public void Configure(IApplicationBuilder app, IReplicationService service, Repl // Running ReplicationService as middleware allows registering other URL patterns so other services // (such as controllers or razor pages) can be served from the same application. -#if FEATURE_ASPNETCORE_ENDPOINT_CONFIG // Only available in .NET 5+ public class ReplicationServiceMiddleware { private readonly RequestDelegate next; @@ -82,27 +73,20 @@ public ReplicationServiceMiddleware(RequestDelegate next, IReplicationService se public async Task InvokeAsync(HttpContext context) { - // LUCENENET: This is to allow synchronous IO to happen for these requests. - // LUCENENET TODO: Allow async operations from Replicator. - var syncIoFeature = context.Features.Get(); - if (syncIoFeature != null) - { - syncIoFeature.AllowSynchronousIO = true; - } - - await Task.Yield(); - service.Perform(context.Request, context.Response); + // LUCENENET: Although the async/await pattern doesn't exist in Java Lucene, this is the recommended + // approach for modern .NET development. + await service.PerformAsync(context.Request, context.Response, context.RequestAborted); // This is a terminating endpoint. Do not call the next delegate/middleware in the pipeline. } } - public static class ReplicationServiceRouteBuilderExtensions + public static partial class ReplicationServiceRouteBuilderExtensions { - public static IEndpointConventionBuilder MapReplicator(this IEndpointRouteBuilder endpoints, string pattern) + public static IEndpointConventionBuilder MapReplicator(this IEndpointRouteBuilder endpoints, string pattern) where TReplicationServiceMiddleware : class { var pipeline = endpoints.CreateApplicationBuilder() - .UseMiddleware() + .UseMiddleware() .Build(); return endpoints @@ -110,5 +94,5 @@ public static IEndpointConventionBuilder MapReplicator(this IEndpointRouteBuilde .WithDisplayName("Replication Service"); } } -#endif } +#endif diff --git a/src/Lucene.Net.Tests.Replicator/Lucene.Net.Tests.Replicator.csproj b/src/Lucene.Net.Tests.Replicator/Lucene.Net.Tests.Replicator.csproj index 7efffa960e..053cb5b896 100644 --- a/src/Lucene.Net.Tests.Replicator/Lucene.Net.Tests.Replicator.csproj +++ b/src/Lucene.Net.Tests.Replicator/Lucene.Net.Tests.Replicator.csproj @@ -52,25 +52,23 @@ $(SetTargetFramework) - - $(SetTargetFramework) - $(SetTargetFramework) + + + + - - - - - + + diff --git a/src/Lucene.Net.Tests.Replicator/ReplicatorTestCase.cs b/src/Lucene.Net.Tests.Replicator/ReplicatorTestCase.cs index fe0c7d60bd..4a80cdbb78 100644 --- a/src/Lucene.Net.Tests.Replicator/ReplicatorTestCase.cs +++ b/src/Lucene.Net.Tests.Replicator/ReplicatorTestCase.cs @@ -1,17 +1,22 @@ using Lucene.Net.Replicator.Http; using Lucene.Net.Replicator.Http.Abstractions; using Lucene.Net.Util; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; using System; using System.Threading.Tasks; -#if FEATURE_ASPNETCORE_ENDPOINT_CONFIG +#if FEATURE_ASPNETCORE_TESTHOST using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +#else +using Lucene.Net.Replicator.Net; #endif + namespace Lucene.Net.Replicator { /* @@ -32,75 +37,9 @@ namespace Lucene.Net.Replicator */ [SuppressCodecs("Lucene3x")] - public class ReplicatorTestCase : LuceneTestCase + public partial class ReplicatorTestCase : LuceneTestCase { - -#if FEATURE_ASPNETCORE_ENDPOINT_CONFIG - /// - /// Call this overload to use to host . - /// - /// The that will be registered as singleton. - /// The that will be registered as singleton. - /// A configured instance. - public static TestServer NewHttpServer(IReplicationService service, MockErrorConfig mockErrorConfig) - { - var builder = new WebHostBuilder() - .ConfigureServices(container => - { - container.AddRouting(); - container.AddSingleton(service); - container.AddSingleton(mockErrorConfig); - container.AddSingleton(); - container.AddSingleton(); - }) - .Configure(app => - { - app.UseRouting(); - - // Middleware so we can mock a server exception and toggle the exception on and off. - app.UseMiddleware(); - - app.UseEndpoints(endpoints => - { - // This is to define the endpoint for Replicator. - // All URLs with the pattern /replicate/{shard?}/{action?} terminate here and any middleware that - // is expected to run for Replicator must be registered before this call. - endpoints.MapReplicator(ReplicationService.REPLICATION_CONTEXT + "/{shard?}/{action?}"); - - endpoints.MapGet("/{controller?}/{action?}/{id?}", async context => - { - // This is just to demonstrate allowing requests to other services/controllers in the same - // application. This isn't required, but is allowed. - await context.Response.WriteAsync("Hello World!"); - }); - }); - }); - var server = new TestServer(builder); - return server; - } -#else - /// - /// Call this overload to use as the Startup Class. - /// - /// The type of startup class. - /// The that will be registered as singleton. - /// The that will be registered as singleton. - /// A configured instance. - public static TestServer NewHttpServer(IReplicationService service, MockErrorConfig mockErrorConfig) where TStartUp : class - { - var builder = new WebHostBuilder() - .ConfigureServices(container => - { - container.AddSingleton(service); - container.AddSingleton(mockErrorConfig); - }) - .UseStartup(); - - var server = new TestServer(builder); - server.BaseAddress = new Uri("http://localhost" + ReplicationService.REPLICATION_CONTEXT); - return server; - } -#endif + // LUCENENET: Moved NewHttpServer() implementations into partial classes for different server stacks /// /// Returns a 's port. @@ -133,6 +72,119 @@ public class HttpResponseException : Exception public object Value { get; set; } } + public class MockErrorConfig + { + public bool RespondWithError { get; set; } = false; + } + } + +#if FEATURE_ASPNETCORE_TESTHOST + public partial class ReplicatorTestCase + { + public static TestServer NewHttpServer(IReplicationService service, MockErrorConfig mockErrorConfig, bool useSynchronousIO, bool useStartupClass) + { + if (useStartupClass) + { + var builder = new WebHostBuilder() + .ConfigureServices(container => + { + container.AddSingleton(service); + container.AddSingleton(mockErrorConfig); + }); + + if (useSynchronousIO) + { + builder.UseStartup(); + } + else + { + builder.UseStartup(); + } + + var server = new TestServer(builder); + server.BaseAddress = new Uri("http://localhost" + ReplicationService.REPLICATION_CONTEXT); + return server; + } + else + { + var builder = new WebHostBuilder() + .ConfigureServices(container => + { + container.AddRouting(); + container.AddSingleton(service); + container.AddSingleton(mockErrorConfig); + if (useSynchronousIO) + { + container.AddSingleton(); + container.AddSingleton(); + } + else + { + container.AddSingleton(); + } + container.AddSingleton(); + }) + .Configure(app => + { + app.UseRouting(); + + // Middleware so we can mock a server exception and toggle the exception on and off. + app.UseMiddleware(); + + if (useSynchronousIO) + { + app.UseMiddleware(); + } + + app.UseEndpoints(endpoints => + { + // This is to define the endpoint for Replicator. + // All URLs with the pattern /replicate/{shard?}/{action?} terminate here and any middleware that + // is expected to run for Replicator must be registered before this call. + string pattern = ReplicationService.REPLICATION_CONTEXT + "/{shard?}/{action?}"; + if (useSynchronousIO) + endpoints.MapReplicator(pattern); + else + endpoints.MapReplicator(pattern); + + endpoints.MapGet("/{controller?}/{action?}/{id?}", async context => + { + // This is just to demonstrate allowing requests to other services/controllers in the same + // application. This isn't required, but is allowed. + await context.Response.WriteAsync("Hello World!"); + }); + }); + }); + var server = new TestServer(builder); + return server; + } + } + + public class EnableSynchronousIOMiddleware + { + private readonly RequestDelegate next; + + public EnableSynchronousIOMiddleware(RequestDelegate next) + { + this.next = next ?? throw new ArgumentNullException(nameof(next)); + } + + public async Task InvokeAsync(HttpContext context) + { + // LUCENENET: This is to allow synchronous IO to happen for these requests. + // Note that in a real-world app this would be set in the configuration, not + // per HTTP request. However, this setting is not recommended in modern production + // applications. + var syncIoFeature = context.Features.Get(); + if (syncIoFeature != null) + { + syncIoFeature.AllowSynchronousIO = true; + } + + await next(context); + } + } + public class MockErrorMiddleware { private readonly RequestDelegate next; @@ -159,10 +211,15 @@ public async Task InvokeAsync(HttpContext context) await next(context); } } - - public class MockErrorConfig + } +#else + public partial class ReplicatorTestCase + { + // LUCENENET: This uses HttpListener from System.Net to test the service where ASP.NET Core is not supported. + public static TestServer NewHttpServer(IReplicationService service, MockErrorConfig mockErrorConfig, bool useSynchronousIO, bool useStartupClass) { - public bool RespondWithError { get; set; } = false; + return new TestServer(service, mockErrorConfig, useSynchronousIO); } } +#endif } diff --git a/src/Lucene.Net.Tests.Replicator/Support/Http/SynchronousReplicationServlet.cs b/src/Lucene.Net.Tests.Replicator/Support/Http/SynchronousReplicationServlet.cs new file mode 100644 index 0000000000..9efbe56726 --- /dev/null +++ b/src/Lucene.Net.Tests.Replicator/Support/Http/SynchronousReplicationServlet.cs @@ -0,0 +1,101 @@ +#if FEATURE_ASPNETCORE_TESTHOST +using Lucene.Net.Replicator.AspNetCore; +using Lucene.Net.Replicator.Http.Abstractions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using System; +using System.Threading.Tasks; + +namespace Lucene.Net.Replicator.Http +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + // IMPORTANT: These tests are primarily to test the synchronous methods that are a direct port from Java. + // However, modern ASP.NET development is primarily done asynchronously, so it is recommended to follow + // the patterns in the ReplicationServlet.cs class rather than this one in most cases. + + // ********************** Option 1: Use a Startup Class ******************************************** + // The startup class must define all middleware (app.Use) before the terminating endpoint (app.Run) + + public class SynchronousReplicationServlet + { + public void Configure(IApplicationBuilder app, IReplicationService service, ReplicatorTestCase.MockErrorConfig mockErrorConfig) + { + // Middleware to throw an exception conditionally from our test server. + app.Use(async (context, next) => + { + if (mockErrorConfig.RespondWithError) + { + throw new ReplicatorTestCase.HttpResponseException(); + } + + await next(); + }); + app.Use(async (context, next) => + { + // LUCENENET: This is to allow synchronous IO to happen for these requests. + var syncIoFeature = context.Features.Get(); + if (syncIoFeature != null) + { + syncIoFeature.AllowSynchronousIO = true; + } + + await next(); + }); + + + app.Run(async (context) => + { + await Task.Yield(); + service.Perform(context.Request, context.Response); + + // This is a terminating endpoint. Do not call the next delegate/middleware in the pipeline. + + }); + } + } + + // ********************** Option 2: Use Middleware with Endpoint Routing ******************************* + // Running ReplicationService as middleware allows registering other URL patterns so other services + // (such as controllers or razor pages) can be served from the same application. + + public class SynchronousReplicationServiceMiddleware + { + private readonly RequestDelegate next; + private readonly IReplicationService service; + + public SynchronousReplicationServiceMiddleware(RequestDelegate next, IReplicationService service) + { + this.next = next ?? throw new ArgumentNullException(nameof(next)); + this.service = service ?? throw new ArgumentNullException(nameof(service)); + } + + public async Task InvokeAsync(HttpContext context) + { + // NOTE: SynchronousIO enabled by middleware + + await Task.Yield(); + service.Perform(context.Request, context.Response); + + // This is a terminating endpoint. Do not call the next delegate/middleware in the pipeline. + } + } +} +#endif diff --git a/src/Lucene.Net.Tests.Replicator/Support/Net/HttpListenerReplicationRequest.cs b/src/Lucene.Net.Tests.Replicator/Support/Net/HttpListenerReplicationRequest.cs new file mode 100644 index 0000000000..be1d772c79 --- /dev/null +++ b/src/Lucene.Net.Tests.Replicator/Support/Net/HttpListenerReplicationRequest.cs @@ -0,0 +1,37 @@ +using Lucene.Net.Replicator.Http.Abstractions; +using System.Net; + +namespace Lucene.Net.Replicator.Net +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + /// + /// A concrete implementation of for supporting + /// . + /// + public class HttpListenerReplicationRequest : IReplicationRequest + { + private readonly HttpListenerRequest _request; + + public HttpListenerReplicationRequest(HttpListenerRequest request) => _request = request; + + public string Path => _request.Url.AbsolutePath; + + public string QueryParam(string name) => _request.QueryString[name]; + } +} diff --git a/src/Lucene.Net.Tests.Replicator/Support/Net/HttpListenerReplicationResponse.cs b/src/Lucene.Net.Tests.Replicator/Support/Net/HttpListenerReplicationResponse.cs new file mode 100644 index 0000000000..91c3a456dd --- /dev/null +++ b/src/Lucene.Net.Tests.Replicator/Support/Net/HttpListenerReplicationResponse.cs @@ -0,0 +1,44 @@ +using Lucene.Net.Replicator.Http.Abstractions; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace Lucene.Net.Replicator.Net +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + /// + /// A concrete implementation of for supporting + /// . + /// + public class HttpListenerReplicationResponse : IReplicationResponse + { + private readonly HttpListenerResponse _response; + + public HttpListenerReplicationResponse(HttpListenerResponse response) => _response = response; + + public int StatusCode + { + get => _response.StatusCode; + set => _response.StatusCode = value; + } + + public Stream Body => _response.OutputStream; + } +} diff --git a/src/Lucene.Net.Tests.Replicator/Support/Net/TestServer.cs b/src/Lucene.Net.Tests.Replicator/Support/Net/TestServer.cs new file mode 100644 index 0000000000..1da5d84ee0 --- /dev/null +++ b/src/Lucene.Net.Tests.Replicator/Support/Net/TestServer.cs @@ -0,0 +1,140 @@ +using Lucene.Net.Replicator.Http.Abstractions; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Lucene.Net.Replicator.Net +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + /// + /// A simple -based test server with an API modeled + /// after Microsoft.AspNetCore.TestHost.TestServer so we can easily swap this + /// implementation in its place. + /// + public class TestServer : IDisposable + { + private readonly HttpListener _listener; + private readonly IReplicationService _service; + private readonly ReplicatorTestCase.MockErrorConfig _mockErrorConfig; + private readonly bool _useSynchronousIO; + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly Task _serverTask; + + public Uri BaseAddress { get; } + + public TestServer( + IReplicationService service, + ReplicatorTestCase.MockErrorConfig mockErrorConfig, + bool useSynchronousIO, + string prefix = "http://localhost:0/") + { + _service = service ?? throw new ArgumentNullException(nameof(service)); + _mockErrorConfig = mockErrorConfig ?? throw new ArgumentNullException(nameof(mockErrorConfig)); + _useSynchronousIO = useSynchronousIO; + + // Auto-select a free port if prefix ends with 0 + if (prefix.EndsWith("0/", StringComparison.Ordinal)) + { + int port = GetFreePort(); + prefix = prefix.Replace("0/", port + "/"); + } + + _listener = new HttpListener(); + _listener.Prefixes.Add(prefix); + BaseAddress = new Uri(prefix); + _listener.Start(); + + // Start listening loop + _serverTask = Task.Run(() => ListenLoopAsync(_cancellationTokenSource.Token)); + } + + private async Task ListenLoopAsync(CancellationToken token) + { + try + { + while (!token.IsCancellationRequested) + { + var context = await _listener.GetContextAsync(); + + // Handle each request in background + _ = Task.Run(async () => + { + var request = new HttpListenerReplicationRequest(context.Request); + var response = new HttpListenerReplicationResponse(context.Response); + + try + { + // Simulate test error condition + if (_mockErrorConfig.RespondWithError) + { + throw new ReplicatorTestCase.HttpResponseException(); + } + + if (_useSynchronousIO) + { + _service.Perform(request, response); + } + else + { + await _service.PerformAsync(request, response, token); + } + } + catch + { + response.StatusCode = 500; + byte[] errorBytes = Encoding.UTF8.GetBytes("Internal Server Error"); + await response.Body.WriteAsync(errorBytes, 0, errorBytes.Length, token); + } + finally + { + await response.Body.FlushAsync(token); + context.Response.Close(); + } + }, token); + } + } + catch (ObjectDisposedException) { } + catch (HttpListenerException) { } + } + + public void Dispose() + { + _cancellationTokenSource.Cancel(); + _listener.Stop(); + _listener.Close(); + try { _serverTask.Wait(); } catch { } + _cancellationTokenSource.Dispose(); + } + + private static int GetFreePort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + public HttpMessageHandler CreateHandler() => new HttpClientHandler(); + } +} diff --git a/src/Lucene.Net.Tests/Support/IO/TestStreamExtensions.cs b/src/Lucene.Net.Tests/Support/IO/TestStreamExtensions.cs index 597f1ed4e4..f66482458b 100644 --- a/src/Lucene.Net.Tests/Support/IO/TestStreamExtensions.cs +++ b/src/Lucene.Net.Tests/Support/IO/TestStreamExtensions.cs @@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; +using System.Threading.Tasks; namespace Lucene.Net.Support.IO { @@ -33,6 +34,8 @@ public class TestStreamExtensions : LuceneTestCase { private Stream stream; + private static readonly string unihw = "\u0048\u0065\u006C\u006C\u006F\u0020\u0057\u006F\u0072\u006C\u0064"; + private const string fileString = "Test_All_Tests\nTest_java_io_BufferedInputStream\nTest_java_io_BufferedOutputStream\nTest_java_io_ByteArrayInputStream\nTest_java_io_ByteArrayOutputStream\nTest_DataInputStream\n"; [Test] @@ -115,6 +118,98 @@ public void TestReadInt64() Assert.AreEqual(9875645283333L, stream.ReadInt64(), "Incorrect long read"); } + // Additional async tests + + [Test] + // LUCENENET note: adapted from test_writeInt() + public async Task TestWriteInt32BigEndianAsync() + { + await stream.WriteInt32BigEndianAsync(9087589); + // Reset the stream so we can read back + ResetStreamForReading(); + int c = await stream.ReadInt32BigEndianAsync(); + Assert.AreEqual(9087589, c, "Incorrect int written (async)"); + } + + [Test] + // LUCENENET note: adapted from test_writeLong() + public async Task TestWriteInt64BigEndianAsync() + { + await stream.WriteInt64BigEndianAsync(908755555456L); + // Reset the stream so we can read back + ResetStreamForReading(); + long c = await stream.ReadInt64BigEndianAsync(); + Assert.AreEqual(908755555456L, c, "Incorrect long written (async)"); + } + + [Test] + // LUCENENET note: adapted from test_writeUTF() + public async Task TestWriteUTFAsync() + { + await stream.WriteUTFAsync(unihw); + // Reset the stream so we can read back + ResetStreamForReading(); + string result = await stream.ReadUTFAsync(); + Assert.AreEqual(unihw, result, "Incorrect string written (async)"); + } + + [Test] + // LUCENENET note: adapted from test_readInt() + public async Task TestReadInt32BigEndianAsync() + { + await stream.WriteInt32BigEndianAsync(768347202); + // Reset the stream so we can read back + ResetStreamForReading(); + int result = await stream.ReadInt32BigEndianAsync(); + Assert.AreEqual(768347202, result, "Incorrect int read (async)"); + } + + [Test] + // LUCENENET note: adapted from test_readLong() + public async Task TestReadInt64BigEndianAsync() + { + await stream.WriteInt64BigEndianAsync(9875645283333L); + // Reset the stream so we can read back + ResetStreamForReading(); + long result = await stream.ReadInt64BigEndianAsync(); + Assert.AreEqual(9875645283333L, result, "Incorrect long read (async)"); + } + + [Test] + // LUCENENET note: adapted from test_readUTF() + public async Task TestReadUTFAsync() + { + await stream.WriteUTFAsync(unihw); + + // Check that the length was written correctly (UTF length + 2 bytes for length header) + long expectedStreamLength = CalculateExpectedUTFStreamLength(unihw); + Assert.AreEqual(expectedStreamLength, stream.Length, "Failed to write string in UTF format"); + + // Reset and read the string + ResetStreamForReading(); + string result = await stream.ReadUTFAsync(); + Assert.AreEqual(unihw, result, "Incorrect string read (async)"); + } + + /// + /// Helper method to calculate expected UTF stream length for validation + /// Matches DataOutput.writeUTF() spec (Java) + /// + private long CalculateExpectedUTFStreamLength(string value) + { + long utfCount = 0; + foreach (char ch in value) + { + if (ch > 0 && ch <= 127) + utfCount++; + else if (ch <= 2047) + utfCount += 2; + else + utfCount += 3; + } + return utfCount + 2; // +2 for the 2-byte length header + } + private void ResetStreamForReading() // LUCENENET - was "OpenDataInputStream" in Harmony tests { // LUCENENET specific - in the Harmony tests, there were separate streams diff --git a/src/Lucene.Net/Support/IO/StreamExtensions.cs b/src/Lucene.Net/Support/IO/StreamExtensions.cs index 513d127b88..080a9e7b2d 100644 --- a/src/Lucene.Net/Support/IO/StreamExtensions.cs +++ b/src/Lucene.Net/Support/IO/StreamExtensions.cs @@ -1,8 +1,12 @@ using J2N.IO; using Lucene.Net.Support.Threading; using System; +using System.Buffers; using System.IO; using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace Lucene.Net.Support.IO { @@ -210,5 +214,177 @@ public static long ReadInt64(this Stream stream) buff[6] << 16 | buff[7] << 24); return (long)((ulong)hi) << 32 | lo; } + + //async versions of the above methods + public static async Task WriteInt32BigEndianAsync(this Stream output, int value, CancellationToken cancellationToken = default) + { + if (output is null) + throw new ArgumentNullException(nameof(output)); + + byte[] buff = new byte[4]; + buff[0] = (byte)(value >> 24); + buff[1] = (byte)(value >> 16); + buff[2] = (byte)(value >> 8); + buff[3] = (byte)value; + + await output.WriteAsync(buff, 0, buff.Length, cancellationToken).ConfigureAwait(false); + } + + public static async Task WriteInt64BigEndianAsync(this Stream output, long value, CancellationToken cancellationToken = default) + { + if (output is null) + throw new ArgumentNullException(nameof(output)); + + byte[] buff = new byte[8]; + buff[0] = (byte)(value >> 56); + buff[1] = (byte)(value >> 48); + buff[2] = (byte)(value >> 40); + buff[3] = (byte)(value >> 32); + buff[4] = (byte)(value >> 24); + buff[5] = (byte)(value >> 16); + buff[6] = (byte)(value >> 8); + buff[7] = (byte)value; + + await output.WriteAsync(buff, 0, buff.Length, cancellationToken).ConfigureAwait(false); + } + + public static async Task WriteUTFAsync(this Stream output, string value, CancellationToken cancellationToken = default) + { + if (output is null) + throw new ArgumentNullException(nameof(output)); + if (value is null) + throw new ArgumentNullException(nameof(value)); + + long utfCount = CountUTFBytes(value); + if (utfCount > ushort.MaxValue) + throw new EncoderFallbackException("Encoded string too long."); + + byte[] buffer = ArrayPool.Shared.Rent((int)utfCount + 2); + try + { + int offset = 0; + offset = WriteInt16BigEndianToBuffer((int)utfCount, buffer, offset); + offset = WriteUTFBytesToBuffer(value, (int)utfCount, buffer, offset); + + await output.WriteAsync(buffer, 0, offset, cancellationToken).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + public static async Task ReadInt32BigEndianAsync(this Stream input, CancellationToken cancellationToken = default) + { + if (input is null) + throw new ArgumentNullException(nameof(input)); + + byte[] buff = new byte[4]; + int read = await input.ReadAsync(buff, 0, buff.Length, cancellationToken).ConfigureAwait(false); + if (read < buff.Length) throw new EndOfStreamException(); + + return (buff[0] << 24) | (buff[1] << 16) | (buff[2] << 8) | buff[3]; + } + + public static async Task ReadInt64BigEndianAsync(this Stream input, CancellationToken cancellationToken = default) + { + if (input is null) + throw new ArgumentNullException(nameof(input)); + + byte[] buff = new byte[8]; + int read = await input.ReadAsync(buff, 0, buff.Length, cancellationToken).ConfigureAwait(false); + if (read < buff.Length) throw new EndOfStreamException(); + + return ((long)buff[0] << 56) | + ((long)buff[1] << 48) | + ((long)buff[2] << 40) | + ((long)buff[3] << 32) | + ((long)buff[4] << 24) | + ((long)buff[5] << 16) | + ((long)buff[6] << 8) | + buff[7]; + } + + public static async Task ReadUTFAsync(this Stream input, CancellationToken cancellationToken = default) + { + if (input is null) + throw new ArgumentNullException(nameof(input)); + + byte[] lenBuff = ArrayPool.Shared.Rent(2); + try + { + int readLen = await input.ReadAsync(lenBuff, 0, 2, cancellationToken).ConfigureAwait(false); + if (readLen < 2) + throw new EndOfStreamException("Unexpected end of stream while reading UTF length."); + + int length = (lenBuff[0] << 8) | lenBuff[1]; + + byte[] buffer = ArrayPool.Shared.Rent(length); + try + { + int read = await input.ReadAsync(buffer, 0, length, cancellationToken).ConfigureAwait(false); + if (read < length) + throw new EndOfStreamException("Unexpected end of stream while reading UTF string."); + + return Encoding.UTF8.GetString(buffer, 0, length); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + finally + { + ArrayPool.Shared.Return(lenBuff); + } + } + + + // ======================== + // Helper methods for UTF + // ======================== + private static long CountUTFBytes(string value) + { + long utfCount = 0; + foreach (char ch in value) + { + if (ch > 0 && ch <= 127) + utfCount++; + else if (ch <= 2047) + utfCount += 2; + else + utfCount += 3; + } + return utfCount; + } + + private static int WriteInt16BigEndianToBuffer(int value, byte[] buffer, int offset) + { + buffer[offset++] = (byte)(value >> 8); + buffer[offset++] = (byte)value; + return offset; + } + + private static int WriteUTFBytesToBuffer(string value, long count, byte[] buffer, int offset) + { + foreach (char ch in value) + { + if (ch > 0 && ch <= 127) + { + buffer[offset++] = (byte)ch; + } + else if (ch <= 2047) + { + buffer[offset++] = (byte)(0xc0 | (0x1f & (ch >> 6))); + buffer[offset++] = (byte)(0x80 | (0x3f & ch)); + } + else + { + buffer[offset++] = (byte)(0xe0 | (0x0f & (ch >> 12))); + buffer[offset++] = (byte)(0x80 | (0x3f & (ch >> 6))); + buffer[offset++] = (byte)(0x80 | (0x3f & ch)); + } + } + return offset; + } } } diff --git a/src/dotnet/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationResponse.cs b/src/dotnet/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationResponse.cs index 4808f7592b..5141a89489 100644 --- a/src/dotnet/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationResponse.cs +++ b/src/dotnet/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationResponse.cs @@ -57,16 +57,5 @@ public int StatusCode /// This simply returns the . /// public Stream Body => response.Body; - - /// - /// Flushes the reponse to the underlying response stream. - /// - /// - /// This simply calls on the . - /// - public void Flush() - { - response.Body.Flush(); - } } } diff --git a/src/dotnet/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationServiceExtentions.cs b/src/dotnet/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationServiceExtentions.cs index b79a8545ee..ce87aa0f84 100644 --- a/src/dotnet/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationServiceExtentions.cs +++ b/src/dotnet/Lucene.Net.Replicator.AspNetCore/AspNetCoreReplicationServiceExtentions.cs @@ -1,6 +1,8 @@ using Lucene.Net.Replicator.Http; using Lucene.Net.Replicator.Http.Abstractions; using Microsoft.AspNetCore.Http; +using System.Threading; +using System.Threading.Tasks; namespace Lucene.Net.Replicator.AspNetCore { @@ -31,5 +33,19 @@ public static void Perform(this IReplicationService self, HttpRequest request, H { self.Perform(new AspNetCoreReplicationRequest(request), new AspNetCoreReplicationResponse(response)); } + /// + /// Extension method that mirrors the signature of using AspNetCore as implementation. + /// + public static async Task PerformAsync( + this IReplicationService self, + HttpRequest request, + HttpResponse response, + CancellationToken cancellationToken = default) + { + await self.PerformAsync( + new AspNetCoreReplicationRequest(request), + new AspNetCoreReplicationResponse(response), + cancellationToken); + } } } diff --git a/src/dotnet/Lucene.Net.Replicator.AspNetCore/Lucene.Net.Replicator.AspNetCore.csproj b/src/dotnet/Lucene.Net.Replicator.AspNetCore/Lucene.Net.Replicator.AspNetCore.csproj index cf6e041633..1595be29b6 100644 --- a/src/dotnet/Lucene.Net.Replicator.AspNetCore/Lucene.Net.Replicator.AspNetCore.csproj +++ b/src/dotnet/Lucene.Net.Replicator.AspNetCore/Lucene.Net.Replicator.AspNetCore.csproj @@ -25,7 +25,7 @@ - net8.0;netstandard2.1;netstandard2.0;net462 + net8.0 Lucene.Net.Replicator.AspNetCore AspNetCore integration of Lucene.Net.Replicator for the Lucene.Net full-text search engine library from The Apache Software Foundation. @@ -34,17 +34,12 @@ $(NoWarn);1591;1573 - - - - -