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
-
-
-
-
-