diff --git a/Directory.Build.props b/Directory.Build.props
index 029539129..488f3c4a0 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -20,9 +20,9 @@
true
snupkg
true
- netcoreapp3.1
+ net10.0
true
- NU1901;NU1902;NU1903;NU1904;CA1724
+ CA1021;CA1062;CA1724;SA1414;
@@ -31,11 +31,11 @@
-
-
-
+
+
+
-
+
diff --git a/examples/Statiq.Web.Examples/Statiq.Web.Examples.csproj b/examples/Statiq.Web.Examples/Statiq.Web.Examples.csproj
index f1d241f36..5c4e01ec0 100644
--- a/examples/Statiq.Web.Examples/Statiq.Web.Examples.csproj
+++ b/examples/Statiq.Web.Examples/Statiq.Web.Examples.csproj
@@ -2,7 +2,7 @@
Exe
- netcoreapp3.1
+ net10.0
$(MSBuildProjectDirectory)
diff --git a/src/Statiq.Web.Aws/Statiq.Web.Aws.csproj b/src/Statiq.Web.Aws/Statiq.Web.Aws.csproj
index cb7ee20fb..44ae5ab36 100644
--- a/src/Statiq.Web.Aws/Statiq.Web.Aws.csproj
+++ b/src/Statiq.Web.Aws/Statiq.Web.Aws.csproj
@@ -4,7 +4,7 @@
Statiq Static StaticContent StaticSite Blog BlogEngine AmazonWebServices AWS
-
+
diff --git a/src/Statiq.Web.Azure/DeploySearchIndex.cs b/src/Statiq.Web.Azure/DeploySearchIndex.cs
index bde24d209..c277611ff 100644
--- a/src/Statiq.Web.Azure/DeploySearchIndex.cs
+++ b/src/Statiq.Web.Azure/DeploySearchIndex.cs
@@ -1,16 +1,15 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Text;
using System.Threading.Tasks;
-using Microsoft.Azure.Search;
-using Microsoft.Azure.Search.Models;
+using Azure;
using Microsoft.Extensions.Logging;
using Polly;
using Statiq.Common;
+using Azure.Search.Documents;
+using Azure.Search.Documents.Indexes;
+using Azure.Search.Documents.Indexes.Models;
+using Azure.Search.Documents.Models;
namespace Statiq.Web.Azure
{
@@ -41,7 +40,7 @@ public DeploySearchIndex(
Config searchServiceName,
Config indexName,
Config apiKey,
- Config> fields)
+ Config> fields)
: base(
new Dictionary
{
@@ -59,70 +58,67 @@ protected override async Task> ExecuteConfigAsync(IDocume
string searchServiceName = values.GetString(SearchServiceName) ?? throw new ExecutionException("Invalid search service name");
string indexName = values.GetString(IndexName) ?? throw new ExecutionException("Invalid search index name");
string apiKey = values.GetString(ApiKey) ?? throw new ExecutionException("Invalid search API key");
- IList fields = values.GetList(Fields)?.ToList() ?? throw new ExecutionException("Invalid search fields");
+ IList fields = values.GetList(Fields)?.ToList() ?? throw new ExecutionException("Invalid search fields");
- SearchServiceClient client = new SearchServiceClient(searchServiceName, new SearchCredentials(apiKey));
-
- // Delete the index if it currently exists (recreating is the easiest way to update it)
- CorsOptions corsOptions = null;
- if (await client.Indexes.ExistsAsync(indexName, null, context.CancellationToken))
+ if (!Uri.TryCreate(searchServiceName, UriKind.Absolute, out Uri searchServiceUri))
{
- // Get the CORS options because we'll need to recreate those
- Microsoft.Azure.Search.Models.Index existingIndex = await client.Indexes.GetAsync(indexName, null, context.CancellationToken);
- corsOptions = existingIndex.CorsOptions;
-
- // Delete the existing index
- context.LogDebug($"Deleting existing search index {indexName}");
- await client.Indexes.DeleteAsync(indexName, null, null, context.CancellationToken);
+ throw new ExecutionException("Invalid search service name");
}
+ SearchIndexClient searchIndexClient = new SearchIndexClient(searchServiceUri, new AzureKeyCredential(apiKey));
+
// Create the index
- Microsoft.Azure.Search.Models.Index index = new Microsoft.Azure.Search.Models.Index
+ SearchIndex index = new SearchIndex(indexName)
{
- Name = indexName,
Fields = fields,
- CorsOptions = corsOptions
};
context.LogDebug($"Creating search index {indexName}");
- await client.Indexes.CreateAsync(index, null, context.CancellationToken);
+ await searchIndexClient.CreateOrUpdateIndexAsync(index, true, true, context.CancellationToken);
// Upload the documents to the search index in batches
context.LogDebug($"Uploading {context.Inputs.Length} documents to search index {indexName}...");
- ISearchIndexClient indexClient = client.Indexes.GetClient(indexName);
+ SearchClient indexClient = searchIndexClient.GetSearchClient(indexName);
int start = 0;
do
{
// Create the dynamic search documents and batch
- IndexAction[] indexActions = context.Inputs
+ IndexDocumentsAction[] indexActions = context.Inputs
.Skip(start)
.Take(BatchSize)
.Select(doc =>
{
- Microsoft.Azure.Search.Models.Document searchDocument = new Microsoft.Azure.Search.Models.Document();
- foreach (Field field in fields)
+ SearchDocument searchDocument = new SearchDocument();
+ foreach (SearchField field in fields)
{
if (doc.ContainsKey(field.Name))
{
searchDocument[field.Name] = doc.Get(field.Name);
}
}
- return IndexAction.Upload(searchDocument);
+
+ return IndexDocumentsAction.Upload(searchDocument);
})
.ToArray();
- IndexBatch indexBatch = IndexBatch.New(indexActions);
+ IndexDocumentsBatch indexBatch = IndexDocumentsBatch.Create(indexActions);
// Upload the batch with exponential retry for failures
await Policy
- .Handle()
- .WaitAndRetryAsync(
- 5,
- attempt =>
- {
- context.LogWarning($"Failure while uploading batch {(start / BatchSize) + 1}, retry number {attempt}");
- return TimeSpan.FromSeconds(Math.Pow(2, attempt));
- },
- (ex, _) => indexBatch = ((IndexBatchException)ex).FindFailedActionsToRetry(indexBatch, fields.Single(x => x.IsKey == true).Name))
- .ExecuteAsync(async ct => await indexClient.Documents.IndexAsync(indexBatch, null, ct), context.CancellationToken);
+ .Handle()
+ .OrResult(r => r.Results.Any(result => !result.Succeeded))
+ .WaitAndRetryAsync(
+ 5,
+ attempt =>
+ {
+ context.LogWarning($"Failure while uploading batch {(start / BatchSize) + 1}, retry number {attempt}");
+ return TimeSpan.FromSeconds(Math.Pow(2, attempt));
+ },
+ (ex, _) =>
+ {
+ IEnumerable failedResults = ex.Result.Results.Where(r => !r.Succeeded).Select(result => result.Key);
+ IEnumerable> failedActions = indexActions.Where(action => action.Document.Keys.Any(key => failedResults.Contains(key)));
+ indexBatch = IndexDocumentsBatch.Create(failedActions.ToArray());
+ })
+ .ExecuteAsync(async ct => await indexClient.IndexDocumentsAsync(indexBatch, null, ct), context.CancellationToken);
context.LogDebug($"Uploaded {start + indexActions.Length} documents to search index {indexName}");
start += 1000;
diff --git a/src/Statiq.Web.Azure/Statiq.Web.Azure.csproj b/src/Statiq.Web.Azure/Statiq.Web.Azure.csproj
index ac486ab8c..4db1e0bd5 100644
--- a/src/Statiq.Web.Azure/Statiq.Web.Azure.csproj
+++ b/src/Statiq.Web.Azure/Statiq.Web.Azure.csproj
@@ -4,8 +4,8 @@
Statiq Static StaticContent StaticSite Blog BlogEngine Azure AppService
-
-
+
+
diff --git a/src/Statiq.Web.GitHub/Statiq.Web.GitHub.csproj b/src/Statiq.Web.GitHub/Statiq.Web.GitHub.csproj
index 29adbb38c..325c361c5 100644
--- a/src/Statiq.Web.GitHub/Statiq.Web.GitHub.csproj
+++ b/src/Statiq.Web.GitHub/Statiq.Web.GitHub.csproj
@@ -18,8 +18,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/src/Statiq.Web.Hosting/Server.cs b/src/Statiq.Web.Hosting/Server.cs
index 3442c4484..6b6350209 100644
--- a/src/Statiq.Web.Hosting/Server.cs
+++ b/src/Statiq.Web.Hosting/Server.cs
@@ -11,21 +11,23 @@
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Statiq.Common;
using Statiq.Web.Hosting.LiveReload;
using Statiq.Web.Hosting.Middleware;
+using Microsoft.AspNetCore.Hosting.Server;
namespace Statiq.Web.Hosting
{
///
/// An HTTP server that can serve static files from a specified directory on disk.
///
- public class Server : IWebHost
+ public class Server : IHost
{
private readonly IReadOnlyDictionary _contentTypes;
private readonly IReadOnlyDictionary _customHeaders;
- private readonly IWebHost _host;
+ private readonly IHost _host;
private readonly LiveReloadServer _liveReloadServer;
public bool Extensionless { get; }
@@ -40,7 +42,8 @@ public class Server : IWebHost
///
public string VirtualDirectory { get; }
- public IFeatureCollection ServerFeatures => _host.ServerFeatures;
+ // IWebHost exposed ServerFeatures directly; IHost does not. Resolve the server and return its features if available.
+ public IFeatureCollection ServerFeatures => _host.Services.GetService()?.Features;
public IServiceProvider Services => _host.Services;
@@ -50,7 +53,7 @@ public class Server : IWebHost
/// The local path to serve files from.
/// The port the server will serve HTTP requests on.
public Server(string localPath, int port = 5080)
- : this(localPath, port, true, null, true, null)
+ : this(localPath, port, true, null, true, null, null, null)
{
}
@@ -113,29 +116,33 @@ public Server(
string currentDirectory = Directory.GetCurrentDirectory();
- _host = new WebHostBuilder()
- .UseContentRoot(currentDirectory)
- .UseWebRoot(Path.Combine(currentDirectory, "wwwroot"))
- .ConfigureLogging(loggingBuilder =>
+ _host = new HostBuilder()
+ .ConfigureWebHost(webHostBuilder =>
{
- if (loggerProviders is object)
- {
- foreach (ILoggerProvider loggerProvider in loggerProviders)
+ webHostBuilder
+ .UseContentRoot(currentDirectory)
+ .UseWebRoot(Path.Combine(currentDirectory, "wwwroot"))
+ .ConfigureLogging(loggingBuilder =>
{
- if (loggerProvider is object)
+ if (loggerProviders != null)
{
- loggingBuilder.AddProvider(
- new ChangeLevelLoggerProvider(
- loggerProvider,
- level => level == LogLevel.Information ? LogLevel.Debug : level));
+ foreach (ILoggerProvider loggerProvider in loggerProviders)
+ {
+ if (loggerProvider != null)
+ {
+ loggingBuilder.AddProvider(
+ new ChangeLevelLoggerProvider(
+ loggerProvider,
+ level => level == LogLevel.Information ? LogLevel.Debug : level));
+ }
+ }
}
- }
- }
+ })
+ .UseKestrel()
+ .ConfigureKestrel(x => x.ListenAnyIP(port))
+ .ConfigureServices(ConfigureServices)
+ .Configure(ConfigureApp);
})
- .UseKestrel()
- .ConfigureKestrel(x => x.ListenAnyIP(port))
- .ConfigureServices(ConfigureServices)
- .Configure(ConfigureApp)
.Build();
}
@@ -158,7 +165,7 @@ private void ConfigureServices(IServiceCollection services)
options.ForwardedHeaders = ForwardedHeaders.All;
// Only loopback proxies are allowed by default, clear that restriction
- options.KnownNetworks.Clear();
+ options.KnownIPNetworks.Clear();
options.KnownProxies.Clear();
});
}
@@ -172,7 +179,7 @@ private void ConfigureApp(IApplicationBuilder app)
IWebHostEnvironment host = app.ApplicationServices.GetService();
host.WebRootFileProvider = compositeFileProvider;
- if (_liveReloadServer is object)
+ if (_liveReloadServer != null)
{
// Inject LiveReload script tags to HTML documents, needs to run first as it overrides output stream
app.UseScriptInjection($"{VirtualDirectory ?? string.Empty}/livereload.js?host=localhost&port={Port}");
@@ -237,7 +244,7 @@ private void ConfigureApp(IApplicationBuilder app)
public async Task TriggerReloadAsync()
{
- if (_liveReloadServer is object)
+ if (_liveReloadServer != null)
{
await _liveReloadServer.SendReloadMessageAsync();
}
diff --git a/src/Statiq.Web.Hosting/Statiq.Web.Hosting.csproj b/src/Statiq.Web.Hosting/Statiq.Web.Hosting.csproj
index 7b972be9c..f4a919f59 100644
--- a/src/Statiq.Web.Hosting/Statiq.Web.Hosting.csproj
+++ b/src/Statiq.Web.Hosting/Statiq.Web.Hosting.csproj
@@ -11,9 +11,7 @@
-
-
-
+
diff --git a/src/Statiq.Web.Netlify/Statiq.Web.Netlify.csproj b/src/Statiq.Web.Netlify/Statiq.Web.Netlify.csproj
index 92f8b4294..8938ce4b6 100644
--- a/src/Statiq.Web.Netlify/Statiq.Web.Netlify.csproj
+++ b/src/Statiq.Web.Netlify/Statiq.Web.Netlify.csproj
@@ -6,7 +6,8 @@
-
+
+
diff --git a/src/Statiq.Web/IExecutionContextXrefExtensions.cs b/src/Statiq.Web/IExecutionContextXrefExtensions.cs
index 33e98c39b..30858e1e1 100644
--- a/src/Statiq.Web/IExecutionContextXrefExtensions.cs
+++ b/src/Statiq.Web/IExecutionContextXrefExtensions.cs
@@ -135,7 +135,7 @@ public static string GetXrefLink(this IExecutionContext context, string xref, bo
.SelectMany(x => context.Outputs.FromPipeline(x).Select(y => (PipelineName: x, Document: y)))
.Select(x => (x.Document.GetString(WebKeys.Xref), x))
.Where(x => x.Item1 is object)
- .GroupBy(x => x.Item1, x => x.Item2, StringComparer.OrdinalIgnoreCase)
+ .GroupBy(x => x.Item1, x => x.x, StringComparer.OrdinalIgnoreCase)
.ToDictionary(x => x.Key, x => (ICollection<(string, IDocument)>)x.ToArray(), StringComparer.OrdinalIgnoreCase);
}
}
\ No newline at end of file
diff --git a/src/Statiq.Web/Statiq.Web.csproj b/src/Statiq.Web/Statiq.Web.csproj
index e9930a12b..55c4c35eb 100644
--- a/src/Statiq.Web/Statiq.Web.csproj
+++ b/src/Statiq.Web/Statiq.Web.csproj
@@ -11,7 +11,10 @@
-
+
+
+
+
diff --git a/src/Statiq.Web/Templates/Templates.cs b/src/Statiq.Web/Templates/Templates.cs
index 0524e9b19..adf5875fe 100644
--- a/src/Statiq.Web/Templates/Templates.cs
+++ b/src/Statiq.Web/Templates/Templates.cs
@@ -15,7 +15,9 @@
namespace Statiq.Web
{
+#pragma warning disable CA1710 // Rename to TemplatesDictionary or TemplatesCollection
public class Templates : IReadOnlyList, IReadOnlyDictionary
+#pragma warning restore CA1710 // Rename to TemplatesDictionary or TemplatesCollection
{
private readonly List> _templates = new List>();
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
index aa2133ccb..39a0bc147 100644
--- a/tests/Directory.Build.props
+++ b/tests/Directory.Build.props
@@ -5,7 +5,7 @@
true
-
-
+
+
\ No newline at end of file
diff --git a/tests/Statiq.Web.Hosting.Tests/Middleware/DefaultExtensionsMiddlewareTests.cs b/tests/Statiq.Web.Hosting.Tests/Middleware/DefaultExtensionsMiddlewareTests.cs
index ae5faadbb..87cf10e55 100644
--- a/tests/Statiq.Web.Hosting.Tests/Middleware/DefaultExtensionsMiddlewareTests.cs
+++ b/tests/Statiq.Web.Hosting.Tests/Middleware/DefaultExtensionsMiddlewareTests.cs
@@ -81,7 +81,8 @@ public async Task DoesNotReturnsFileWithNonDefaultExtension()
response.StatusCode.ShouldBe(System.Net.HttpStatusCode.NotFound);
body.ShouldBe(string.Empty);
}
-
+#pragma warning disable ASPDEPR004 // WebHostBuilder Obsolete - needs to be replaced with WebApplicationFactory
+#pragma warning disable ASPDEPR008 // WebHostBuilder Obsolete - needs to be replaced with WebApplicationFactory
private TestServer GetServer(DefaultExtensionsOptions options) =>
new TestServer(
new WebHostBuilder()
diff --git a/tests/Statiq.Web.Hosting.Tests/Middleware/DisableCacheMiddlewareTests.cs b/tests/Statiq.Web.Hosting.Tests/Middleware/DisableCacheMiddlewareTests.cs
index 29a37b5dd..6e14528dd 100644
--- a/tests/Statiq.Web.Hosting.Tests/Middleware/DisableCacheMiddlewareTests.cs
+++ b/tests/Statiq.Web.Hosting.Tests/Middleware/DisableCacheMiddlewareTests.cs
@@ -29,7 +29,8 @@ public async Task AddsCacheHeaders()
response.Headers.GetValues("Pragma").ShouldContain("no-cache");
response.Content.Headers.GetValues("Expires").ShouldContain("0");
}
-
+#pragma warning disable ASPDEPR004 // WebHostBuilder Obsolete - needs to be replaced with WebApplicationFactory
+#pragma warning disable ASPDEPR008 // WebHostBuilder Obsolete - needs to be replaced with WebApplicationFactory
private TestServer GetServer() => new TestServer(
new WebHostBuilder()
.Configure(app => app.UseDisableCache()));
diff --git a/tests/Statiq.Web.Hosting.Tests/Middleware/ScriptInjectionMiddlewareTests.cs b/tests/Statiq.Web.Hosting.Tests/Middleware/ScriptInjectionMiddlewareTests.cs
index 760af5506..8611e8691 100644
--- a/tests/Statiq.Web.Hosting.Tests/Middleware/ScriptInjectionMiddlewareTests.cs
+++ b/tests/Statiq.Web.Hosting.Tests/Middleware/ScriptInjectionMiddlewareTests.cs
@@ -45,7 +45,8 @@ public async Task ShouldNotInjectNonHtmlContent()
// Then
body.ShouldBe(AssemblyHelper.ReadEmbeddedWebFile("NonHtmlDocument.css"));
}
-
+#pragma warning disable ASPDEPR004 // WebHostBuilder Obsolete - needs to be replaced with WebApplicationFactory
+#pragma warning disable ASPDEPR008 // WebHostBuilder Obsolete - needs to be replaced with WebApplicationFactory
private TestServer GetServer() => new TestServer(
new WebHostBuilder()
.Configure(app => app
diff --git a/tests/Statiq.Web.Hosting.Tests/Middleware/VirtualDirectoryMiddlewareTests.cs b/tests/Statiq.Web.Hosting.Tests/Middleware/VirtualDirectoryMiddlewareTests.cs
index 71a59b697..9b03053c2 100644
--- a/tests/Statiq.Web.Hosting.Tests/Middleware/VirtualDirectoryMiddlewareTests.cs
+++ b/tests/Statiq.Web.Hosting.Tests/Middleware/VirtualDirectoryMiddlewareTests.cs
@@ -59,7 +59,8 @@ public async Task DoesNotGetFileNotUnderVirtualDirectory()
// Then
response.StatusCode.ShouldBe(System.Net.HttpStatusCode.NotFound);
}
-
+#pragma warning disable ASPDEPR004 // WebHostBuilder Obsolete - needs to be replaced with WebApplicationFactory
+#pragma warning disable ASPDEPR008 // WebHostBuilder Obsolete - needs to be replaced with WebApplicationFactory
private TestServer GetServer(string virtualDirectory) => new TestServer(
new WebHostBuilder()
.Configure(app => app
diff --git a/tests/Statiq.Web.Hosting.Tests/Statiq.Web.Hosting.Tests.csproj b/tests/Statiq.Web.Hosting.Tests/Statiq.Web.Hosting.Tests.csproj
index da55d4e89..5414f39ef 100644
--- a/tests/Statiq.Web.Hosting.Tests/Statiq.Web.Hosting.Tests.csproj
+++ b/tests/Statiq.Web.Hosting.Tests/Statiq.Web.Hosting.Tests.csproj
@@ -14,8 +14,8 @@
-
-
+
+
diff --git a/tests/Statiq.Web.Tests/Statiq.Web.Tests.csproj b/tests/Statiq.Web.Tests/Statiq.Web.Tests.csproj
index 65e983b55..c161dadab 100644
--- a/tests/Statiq.Web.Tests/Statiq.Web.Tests.csproj
+++ b/tests/Statiq.Web.Tests/Statiq.Web.Tests.csproj
@@ -1,7 +1,4 @@
-
-
-