diff --git a/FunderMaps.sln b/FunderMaps.sln index 527d62389..4bc26ef17 100644 --- a/FunderMaps.sln +++ b/FunderMaps.sln @@ -35,6 +35,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunderMaps.Portal", "src\Fu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunderMaps.BatchNode", "src\FunderMaps.BatchNode\FunderMaps.BatchNode.csproj", "{532227C4-BAD4-46AB-B45D-65F15317AFFA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunderMaps.Cli", "src\FunderMaps.Cli\FunderMaps.Cli.csproj", "{ACDD237D-7C7E-425B-88CD-EF90A0037505}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -89,6 +91,10 @@ Global {532227C4-BAD4-46AB-B45D-65F15317AFFA}.Debug|Any CPU.Build.0 = Debug|Any CPU {532227C4-BAD4-46AB-B45D-65F15317AFFA}.Release|Any CPU.ActiveCfg = Release|Any CPU {532227C4-BAD4-46AB-B45D-65F15317AFFA}.Release|Any CPU.Build.0 = Release|Any CPU + {ACDD237D-7C7E-425B-88CD-EF90A0037505}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACDD237D-7C7E-425B-88CD-EF90A0037505}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACDD237D-7C7E-425B-88CD-EF90A0037505}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACDD237D-7C7E-425B-88CD-EF90A0037505}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -106,6 +112,7 @@ Global {2A345D40-2842-4AAA-A552-630FAD97B9D5} = {0219D434-DD4F-4187-ADED-8CD4A62E90F1} {CF0E7A1A-20A8-4A15-8B57-D7218B9F33D1} = {2E7D6F8C-F448-46D2-88BA-2CA8DD7836E9} {532227C4-BAD4-46AB-B45D-65F15317AFFA} = {2E7D6F8C-F448-46D2-88BA-2CA8DD7836E9} + {ACDD237D-7C7E-425B-88CD-EF90A0037505} = {2E7D6F8C-F448-46D2-88BA-2CA8DD7836E9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CDA50611-E011-4C1E-8823-7A6943652DCE} diff --git a/contrib/FunderMapsCli/.gitignore b/contrib/FunderMapsCli/.gitignore deleted file mode 100644 index 4b2410fb2..000000000 --- a/contrib/FunderMapsCli/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Protos diff --git a/contrib/FunderMapsCli/Makefile b/contrib/FunderMapsCli/Makefile deleted file mode 100644 index 88b9e10ce..000000000 --- a/contrib/FunderMapsCli/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -all: protos - -protos: - python3 -m grpc_tools.protoc -I ../../src/FunderMaps.Infrastructure/ --python_out=. --grpc_python_out=. ../../src/FunderMaps.Infrastructure/Protos/*.proto diff --git a/contrib/FunderMapsCli/batch_client.py b/contrib/FunderMapsCli/batch_client.py deleted file mode 100755 index fd5f59c3c..000000000 --- a/contrib/FunderMapsCli/batch_client.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright 2020 FunderMaps B.V. -# -# Licensed 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. - -""" FunderMaps Commandline Interface """ - -import logging -import json -import sys - -import grpc - -from Protos import protocol_pb2 -from Protos import batch_pb2 -from Protos import batch_pb2_grpc -from google.protobuf import empty_pb2 - -GRPC_HOST = 'localhost:5000' - - -def _make_protocol(): - proto_version = 0xa1 - return protocol_pb2.FunderMapsProtocol( - version=proto_version, - user_agent='batch_client.py') - - -def _print_response(response): - if not response.task_id: - print("Server returned an error") - else: - print(f"Task ID : {response.task_id}") - - -def bundle_create(stub, bundle): - print("-------------- Bundle --------------") - print(f"Bundle : {bundle}") - - request = batch_pb2.EnqueueRequest( - protocol=_make_protocol(), - name="bundle_building", - payload=json.dumps({ - "BundleId": bundle, - "Formats": [0, 1]}) - ) - - response = stub.Enqueue(request) - _print_response(response) - - -def bundle_batch(stub): - print("-------------- Bundle --------------") - print(f"Bundle : all") - - request = batch_pb2.EnqueueRequest( - protocol=_make_protocol(), - name="bundle_batch", - payload=json.dumps({ - "Formats": [0, 1]}) - ) - - response = stub.Enqueue(request) - _print_response(response) - - -def foobar(stub): - print("-------------- Foobar --------------") - - request = batch_pb2.EnqueueRequest( - protocol=_make_protocol(), - name="foobar") - - response = stub.Enqueue(request) - _print_response(response) - - -def email_send(stub, recp): - print("-------------- Email --------------") - - request = batch_pb2.EnqueueRequest( - protocol=_make_protocol(), - name="incident_notify", - payload=json.dumps({ - 'recipients': [recp], - })) - - response = stub.Enqueue(request) - _print_response(response) - - -def get_status(stub): - print("-------------- Status --------------") - - response = stub.Status(empty_pb2.Empty()) - print(f"Jobs succeeded: {response.jobs_succeeded}") - print(f"Jobs failed: {response.jobs_failed}") - - -def run(): - # NOTE: .close() is possible on a channel and should be used in circumstances - # in which the with statement does not fit the needs of the code. - creds = grpc.ssl_channel_credentials() - with grpc.secure_channel(GRPC_HOST, creds) as channel: - # with grpc.insecure_channel(GRPC_HOST) as channel: - stub = batch_pb2_grpc.BatchStub(channel) - if len(sys.argv) == 1: - print('Command required') - return - - if sys.argv[1] == 'foobar': - foobar(stub) - elif sys.argv[1] == 'email': - email_send(stub, sys.argv[2]) - elif sys.argv[1] == 'bundle': - if sys.argv[2] == 'all': - bundle_batch(stub) - else: - bundle_create(stub, sys.argv[2]) - elif sys.argv[1] == 'status': - get_status(stub) - else: - print('Command required') - - -if __name__ == '__main__': - logging.basicConfig() - run() diff --git a/src/FunderMaps.AspNetCore/FunderMaps.AspNetCore.csproj b/src/FunderMaps.AspNetCore/FunderMaps.AspNetCore.csproj index 6d32b2073..a89da1a7a 100644 --- a/src/FunderMaps.AspNetCore/FunderMaps.AspNetCore.csproj +++ b/src/FunderMaps.AspNetCore/FunderMaps.AspNetCore.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/FunderMaps.BatchNode/Command/CommandInfo.cs b/src/FunderMaps.BatchNode/Command/CommandInfo.cs index a92fe57b4..e2df6683d 100644 --- a/src/FunderMaps.BatchNode/Command/CommandInfo.cs +++ b/src/FunderMaps.BatchNode/Command/CommandInfo.cs @@ -6,7 +6,7 @@ namespace FunderMaps.BatchNode.Command /// /// Command descriptor. /// - public class CommandInfo + public class CommandInfo // TODO: Move to core { /// /// Command filename. diff --git a/src/FunderMaps.BatchNode/Jobs/BundleBuilder/BundleBatch.cs b/src/FunderMaps.BatchNode/Jobs/BundleBuilder/BundleBatch.cs index f6084a1b5..c8c0fd94f 100644 --- a/src/FunderMaps.BatchNode/Jobs/BundleBuilder/BundleBatch.cs +++ b/src/FunderMaps.BatchNode/Jobs/BundleBuilder/BundleBatch.cs @@ -14,7 +14,7 @@ namespace FunderMaps.BatchNode.Jobs.BundleBuilder /// /// Bundle batch job entry. /// - internal class BundleBatch : CommandTask + internal class BundleBatch : CommandTask // TODO: Rename to BundleAllJob { private const string TaskName = "BUNDLE_BATCH"; diff --git a/src/FunderMaps.BatchNode/Jobs/BundleBuilder/BundleBuildingContext.cs b/src/FunderMaps.BatchNode/Jobs/BundleBuilder/BundleBuildingContext.cs index 12a5c78bf..d79ceb5f0 100644 --- a/src/FunderMaps.BatchNode/Jobs/BundleBuilder/BundleBuildingContext.cs +++ b/src/FunderMaps.BatchNode/Jobs/BundleBuilder/BundleBuildingContext.cs @@ -7,7 +7,7 @@ namespace FunderMaps.BatchNode.Jobs.BundleBuilder /// /// Context for executing a bundle build task. /// - public class BundleBuildingContext + public class BundleBuildingContext // TODO: Remove, this is moved to core { /// /// The id of the bundle to process. diff --git a/src/FunderMaps.BatchNode/Jobs/BundleBuilder/BundleLayerSource.cs b/src/FunderMaps.BatchNode/Jobs/BundleBuilder/BundleLayerSource.cs index 4931af815..296ea271f 100644 --- a/src/FunderMaps.BatchNode/Jobs/BundleBuilder/BundleLayerSource.cs +++ b/src/FunderMaps.BatchNode/Jobs/BundleBuilder/BundleLayerSource.cs @@ -32,9 +32,9 @@ public BundleLayerSource(Bundle bundle, Layer layer, string workspace) layerOutputName = layer.Slug; // Select layer field as follows: - // * If no column is specified then select everything from the layer - // * If wildcard is found, then only use wildcard. - // * If no geometry column was found, then add one. + // - If no column is specified then select everything from the layer + // - If wildcard is found, then only use wildcard. + // - If no geometry column was found, then add one. List columns = new(configuration.ColumnNames); if (columns.Count == 0) { @@ -65,4 +65,4 @@ public override void Imbue(CommandInfo commandInfo) commandInfo.ArgumentList.Add(layerOutputName); } } -} \ No newline at end of file +} diff --git a/src/FunderMaps.BatchNode/Program.cs b/src/FunderMaps.BatchNode/Program.cs index 63e5b2409..49df84a1a 100644 --- a/src/FunderMaps.BatchNode/Program.cs +++ b/src/FunderMaps.BatchNode/Program.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Hosting; using System.Threading.Tasks; @@ -29,6 +30,13 @@ public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { + webBuilder.ConfigureKestrel(options => + { + options.ConfigureEndpointDefaults(config => + { + config.Protocols = HttpProtocols.Http2; + }); + }); webBuilder.UseStartup(); }); } diff --git a/src/FunderMaps.BatchNode/appsettings.json b/src/FunderMaps.BatchNode/appsettings.json index c542d1a25..ca2a86a4a 100644 --- a/src/FunderMaps.BatchNode/appsettings.json +++ b/src/FunderMaps.BatchNode/appsettings.json @@ -21,10 +21,5 @@ "SecretKey": "", "ServiceUri": "", "BlobStorageName": "" - }, - "Kestrel": { - "EndpointDefaults": { - "Protocols": "Http2" - } } -} +} \ No newline at end of file diff --git a/src/FunderMaps.Cli/CommandDriver.cs b/src/FunderMaps.Cli/CommandDriver.cs new file mode 100644 index 000000000..f29dfb5ad --- /dev/null +++ b/src/FunderMaps.Cli/CommandDriver.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace FunderMaps.Cli.Drivers +{ + internal abstract class CommandDriver + { + protected static Task ResolveSelfScope(IServiceProvider services, Func shadowFunc) + { + var scopeFactory = services.GetRequiredService(); + using var serviceScope = scopeFactory.CreateScope(); + var serviceProvider = serviceScope.ServiceProvider; + TDriver driver = serviceProvider.GetService(); + return shadowFunc(driver); + } + } +} diff --git a/src/FunderMaps.Cli/DriverHandler.cs b/src/FunderMaps.Cli/DriverHandler.cs new file mode 100644 index 000000000..27a71fc3d --- /dev/null +++ b/src/FunderMaps.Cli/DriverHandler.cs @@ -0,0 +1,38 @@ +using System; +using System.CommandLine.Invocation; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace FunderMaps.Cli +{ + /// + /// Driver command handler. + /// + public static class DriverHandler + { + /// + /// Create delegate with default parametes. + /// + /// Handler action. + /// See . + public static ICommandHandler InstantiateDriver(Func action) + => CommandHandler.Create(action); + + /// + /// Create delegate with default parametes. + /// + /// Handler action. + /// See . + public static ICommandHandler InstantiateDriver(Func action) + => CommandHandler.Create(action); + + /// + /// Create delegate with default parametes. + /// + /// Handler action. + /// See . + public static ICommandHandler InstantiateDriver(Func action) + => CommandHandler.Create(action); + } +} diff --git a/src/FunderMaps.Cli/Drivers/BatchDriver.cs b/src/FunderMaps.Cli/Drivers/BatchDriver.cs new file mode 100644 index 000000000..e32ac88e0 --- /dev/null +++ b/src/FunderMaps.Cli/Drivers/BatchDriver.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FunderMaps.Core.Interfaces; +using FunderMaps.Core.MapBundle; +using FunderMaps.Core.Threading; +using FunderMaps.Core.Types; +using Microsoft.Extensions.Hosting; + +namespace FunderMaps.Cli.Drivers +{ + /// + /// gRPC channel factory. + /// + internal class BatchDriver : CommandDriver + { + private readonly IBatchService _batchService; + private readonly IBundleService _bundleService; + + /// + /// Create new instance. + /// + public BatchDriver(IBatchService batchService, IBundleService bundleService) + => (_batchService, _bundleService) = (batchService, bundleService); + + private async Task StatusAsync(CancellationToken token = default) + { + DispatchManagerStatus status = await _batchService.StatusAsync(token); + Console.WriteLine($"Jobs failed: {status.JobsFailed}"); + Console.WriteLine($"Jobs succeeded: {status.JobsSucceeded}"); + } + + private async Task BuildBundleAsync(IEnumerable bundleIdList, CancellationToken token = default) + { + foreach (var bundleId in bundleIdList) + { + await _bundleService.BuildAsync(new BundleBuildingContext + { + BundleId = bundleId, + Formats = new List { GeometryFormat.GeoPackage }, + }); + } + } + + private async Task BuildAllAsync(CancellationToken token = default) + { + await _bundleService.BuildAllAsync(new BundleBuildingContext + { + Formats = new List { GeometryFormat.GeoPackage }, + }); + } + + #region Factory Methods + + public static Task StatusAsync(FileInfo config, IHost host) + => ResolveSelfScope(host.Services, self => self.StatusAsync()); + + public static Task BuildBundleAsync(FileInfo config, IHost host, IEnumerable bundleId) + => ResolveSelfScope(host.Services, self => self.BuildBundleAsync(bundleId)); + + public static Task BuildAllAsync(FileInfo config, IHost host) + => ResolveSelfScope(host.Services, self => self.BuildAllAsync()); + + #endregion Factory Methods + } +} diff --git a/src/FunderMaps.Cli/FunderMaps.Cli.csproj b/src/FunderMaps.Cli/FunderMaps.Cli.csproj new file mode 100644 index 000000000..0d4f3a5ec --- /dev/null +++ b/src/FunderMaps.Cli/FunderMaps.Cli.csproj @@ -0,0 +1,23 @@ + + + + + Exe + net5.0 + + + + $(SolutionDir)DocumentationFunderMapsCli.xml + + + + + + + + + + + + + diff --git a/src/FunderMaps.Cli/Program.cs b/src/FunderMaps.Cli/Program.cs new file mode 100644 index 000000000..e5a5527f9 --- /dev/null +++ b/src/FunderMaps.Cli/Program.cs @@ -0,0 +1,110 @@ +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Hosting; +using Microsoft.Extensions.Hosting; +using System.CommandLine.Parsing; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using System; +using System.IO; +using FunderMaps.Cli.Drivers; +using Microsoft.Extensions.Logging; + +namespace FunderMaps.Cli +{ + /// + /// Application entry. + /// + public static class Program + { + /// + /// Application entry point. + /// + /// Commandline arguments. + public static Task Main(string[] args) + => CreateHostBuilder(args).Build().InvokeAsync(args); + + /// + /// Build a host and run the application. + /// + /// + /// The signature of this method should not be changed. + /// External tooling expects this function be present. + /// + /// Commandline arguments. + /// See . + public static CommandLineBuilder CreateHostBuilder(string[] args) + => BuildCommandLine() + .AddLocalCommands() + .UseHost(_ => Host.CreateDefaultBuilder(), + host => + { + host.ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddConsole(); + logging.SetMinimumLevel(LogLevel.Error); + }); + host.ConfigureServices(services => + { + // Configure FunderMaps services. + services.AddFunderMapsCoreServices(); + services.AddFunderMapsInfrastructureServices(); + + services.AddTransient(); + }); + }) + .UseDefaults(); + + /// + /// Build commandline builder. + /// + /// See . + private static CommandLineBuilder BuildCommandLine() + { + RootCommand root = new(); + root.AddGlobalOption(new Option(new[] { "-c", "--config" }, "An option whose argument is parsed as a FileInfo")); + root.Description = "FunderMaps CommandLine Interface"; + + return new(root); + } + + /// + /// Register local commands. + /// + /// See . + private static CommandLineBuilder AddLocalCommands(this CommandLineBuilder rootBuilder) + { + Command command = new("batch", "Run batch operations"); + command.AddAlias("b"); + command.AddOption(new Option(new[] { "-H", "--host" }, "The batch host to execute the command on")); + command.AddOption(new Option("--ignore-cert", "Ignore TLS certificate checks")); + + { + Command subcommand = new("build-bundle", "The maplayer bundle(s)"); + subcommand.AddArgument(new Argument("bundleId", "(Optional) Bundle identifiers to build") + { + Arity = ArgumentArity.OneOrMore, + }); + subcommand.Handler = DriverHandler.InstantiateDriver>(BatchDriver.BuildBundleAsync); + command.AddCommand(subcommand); + } + + { + Command subcommand = new("build-bundle-all", "The all maplayer bundles"); + subcommand.Handler = DriverHandler.InstantiateDriver(BatchDriver.BuildAllAsync); + command.AddCommand(subcommand); + } + + { + Command subcommand = new("status", "Print the batch status reports"); + subcommand.Handler = DriverHandler.InstantiateDriver(BatchDriver.StatusAsync); + command.AddCommand(subcommand); + } + + rootBuilder.AddCommand(command); + return rootBuilder; + } + } +} diff --git a/src/FunderMaps.Core/DataAnnotations/GuidAttribute.cs b/src/FunderMaps.Core/DataAnnotations/GuidAttribute.cs index 5f8434ef3..5286ede8c 100644 --- a/src/FunderMaps.Core/DataAnnotations/GuidAttribute.cs +++ b/src/FunderMaps.Core/DataAnnotations/GuidAttribute.cs @@ -7,7 +7,7 @@ namespace FunderMaps.Core.DataAnnotations /// Guid validation attribute. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] - public class GuidAttribute : ValidationAttribute + public class GuidAttribute : ValidationAttribute // TODO: Remove { /// /// Returns true if the value is an non empty guid. diff --git a/src/FunderMaps.Core/Extensions/FunderMapsCoreServiceCollectionExtensions.cs b/src/FunderMaps.Core/Extensions/FunderMapsCoreServiceCollectionExtensions.cs index d416d4257..183b6821d 100644 --- a/src/FunderMaps.Core/Extensions/FunderMapsCoreServiceCollectionExtensions.cs +++ b/src/FunderMaps.Core/Extensions/FunderMapsCoreServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using FunderMaps.Core.Components; using FunderMaps.Core.Interfaces; +using FunderMaps.Core.MapBundle; using FunderMaps.Core.Notification; using FunderMaps.Core.Services; using FunderMaps.Core.Threading; @@ -88,6 +89,7 @@ public static IServiceCollection AddFunderMapsCoreServices(this IServiceCollecti // Register core services in DI container. services.AddScoped(); services.AddScoped(); + services.AddScoped(); // Register core services in DI container. // NOTE: These services take time to initialize are used more often. Registering diff --git a/src/FunderMaps.Core/FunderMaps.Core.csproj b/src/FunderMaps.Core/FunderMaps.Core.csproj index 7fc8780cb..3d3705ba8 100644 --- a/src/FunderMaps.Core/FunderMaps.Core.csproj +++ b/src/FunderMaps.Core/FunderMaps.Core.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/FunderMaps.Core/Interfaces/IBatchService.cs b/src/FunderMaps.Core/Interfaces/IBatchService.cs index c02db69a4..d90786d6a 100644 --- a/src/FunderMaps.Core/Interfaces/IBatchService.cs +++ b/src/FunderMaps.Core/Interfaces/IBatchService.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using FunderMaps.Core.Threading; namespace FunderMaps.Core.Interfaces { @@ -14,10 +15,16 @@ public interface IBatchService /// /// Name of task to run. /// Task payload. - /// Canellation token. + /// Cancellation token. /// Task identifier if task was enqueued. Task EnqueueAsync(string name, object value, CancellationToken token = default); + /// + /// Batch service processing status. + /// + /// Cancellation token. + Task StatusAsync(CancellationToken token = default); + /// /// Test the batch service backend. /// diff --git a/src/FunderMaps.Core/MapBundle/BundleBuildingContext.cs b/src/FunderMaps.Core/MapBundle/BundleBuildingContext.cs new file mode 100644 index 000000000..dda453cb7 --- /dev/null +++ b/src/FunderMaps.Core/MapBundle/BundleBuildingContext.cs @@ -0,0 +1,22 @@ +using FunderMaps.Core.Types; +using System; +using System.Collections.Generic; + +namespace FunderMaps.Core.MapBundle +{ + /// + /// Context for executing a bundle build task. + /// + public record BundleBuildingContext + { + /// + /// The id of the bundle to process. + /// + public Guid BundleId { get; init; } + + /// + /// Contains all formats we wish to export. + /// + public IEnumerable Formats { get; set; } + } +} diff --git a/src/FunderMaps.Core/MapBundle/BundleHub.cs b/src/FunderMaps.Core/MapBundle/BundleHub.cs new file mode 100644 index 000000000..67b588702 --- /dev/null +++ b/src/FunderMaps.Core/MapBundle/BundleHub.cs @@ -0,0 +1,40 @@ +using FunderMaps.Core.Abstractions; +using FunderMaps.Core.Interfaces; +using System; +using System.Threading.Tasks; + +#pragma warning disable CA1812 // Internal class is never instantiated +namespace FunderMaps.Core.MapBundle +{ + /// + /// Process bundles. + /// + internal class BundleHub : AppServiceBase, IBundleService + { + private const string TaskBuildName = "BUNDLE_BUILDING"; + private const string TaskBuildAllName = "BUNDLE_BATCH"; + + private readonly IBatchService _batchService; + + /// + /// Create new instance. + /// + public BundleHub(AppContext appContext, IBatchService batchService) + => (AppContext, _batchService) = (appContext, batchService); + + /// + /// Build a bundle. + /// + /// Bundle building context. + public Task BuildAsync(BundleBuildingContext context) + => _batchService.EnqueueAsync(TaskBuildName, context, AppContext.CancellationToken); + + /// + /// Build all bundles. + /// + /// Bundle building context. + public Task BuildAllAsync(BundleBuildingContext context) + => _batchService.EnqueueAsync(TaskBuildAllName, context, AppContext.CancellationToken); + } +} +#pragma warning restore CA1812 // Internal class is never instantiated diff --git a/src/FunderMaps.Core/MapBundle/IBundleService.cs b/src/FunderMaps.Core/MapBundle/IBundleService.cs new file mode 100644 index 000000000..7ffb9be3e --- /dev/null +++ b/src/FunderMaps.Core/MapBundle/IBundleService.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace FunderMaps.Core.MapBundle +{ + /// + /// Bundle service. + /// + public interface IBundleService + { + /// + /// Build a bundle. + /// + /// Bundle building context. + Task BuildAsync(BundleBuildingContext context); + + /// + /// Build all bundles. + /// + /// Bundle building context. + Task BuildAllAsync(BundleBuildingContext context); + } +} diff --git a/src/FunderMaps.Core/Services/NullBatchService.cs b/src/FunderMaps.Core/Services/NullBatchService.cs index 6c6b8e0f2..e67e75eda 100644 --- a/src/FunderMaps.Core/Services/NullBatchService.cs +++ b/src/FunderMaps.Core/Services/NullBatchService.cs @@ -1,4 +1,5 @@ using FunderMaps.Core.Interfaces; +using FunderMaps.Core.Threading; using System; using System.Threading; using System.Threading.Tasks; @@ -12,14 +13,12 @@ namespace FunderMaps.Core.Services internal class NullBatchService : IBatchService { public Task EnqueueAsync(string name, object value, CancellationToken token = default) - { - return Task.FromResult(Guid.NewGuid()); - } + => Task.FromResult(Guid.NewGuid()); - public Task TestService() - { - return Task.CompletedTask; - } + public Task StatusAsync(CancellationToken token = default) + => Task.FromResult(new DispatchManagerStatus()); + + public Task TestService() => Task.CompletedTask; } } #pragma warning restore CA1812 // Internal class is never instantiated diff --git a/src/FunderMaps.Core/Types/BuildingType.cs b/src/FunderMaps.Core/Types/BuildingType.cs index 72533dfbd..872296aea 100644 --- a/src/FunderMaps.Core/Types/BuildingType.cs +++ b/src/FunderMaps.Core/Types/BuildingType.cs @@ -16,9 +16,9 @@ public enum BuildingType Shed = 1, /// - /// House boat. + /// Houseboat. /// - HouseBoat = 2, + Houseboat = 2, /// /// Mobile home. diff --git a/src/FunderMaps.Data/Providers/NpgsqlDbProvider.cs b/src/FunderMaps.Data/Providers/NpgsqlDbProvider.cs index 7b550b4a2..215b774ed 100644 --- a/src/FunderMaps.Data/Providers/NpgsqlDbProvider.cs +++ b/src/FunderMaps.Data/Providers/NpgsqlDbProvider.cs @@ -49,6 +49,7 @@ static NpgsqlDbProvider() NpgsqlConnection.GlobalTypeMapper.MapEnum("application.role"); NpgsqlConnection.GlobalTypeMapper.MapEnum("report.audit_status"); NpgsqlConnection.GlobalTypeMapper.MapEnum("report.base_measurement_level"); + NpgsqlConnection.GlobalTypeMapper.MapEnum("geocoder.building_type"); NpgsqlConnection.GlobalTypeMapper.MapEnum("maplayer.bundle_status"); NpgsqlConnection.GlobalTypeMapper.MapEnum("report.construction_pile"); NpgsqlConnection.GlobalTypeMapper.MapEnum("report.crack_type"); diff --git a/src/FunderMaps.Infrastructure/BatchClient/BatchClient.cs b/src/FunderMaps.Infrastructure/BatchClient/BatchClient.cs index 68f191623..7316cd654 100644 --- a/src/FunderMaps.Infrastructure/BatchClient/BatchClient.cs +++ b/src/FunderMaps.Infrastructure/BatchClient/BatchClient.cs @@ -12,7 +12,7 @@ namespace FunderMaps.Infrastructure.BatchClient /// /// Client connector to the batch node. /// - internal class BatchClient : IBatchService // TODO: Inherit from AppServiceBase + internal class BatchClient : IBatchService // TODO: Inherit from AppServiceBase // TODO: Rename to BatchProxy { private const string UserAgent = "FunderMaps.Infrastructure"; @@ -29,7 +29,7 @@ public BatchClient(ChannelFactory channelFactory) /// /// Name of task to run. /// Task payload. - /// Canellation token. + /// Cancellation token. public async Task EnqueueAsync(string name, object value = null, CancellationToken token = default) { EnqueueRequest request = new() @@ -39,19 +39,19 @@ public async Task EnqueueAsync(string name, object value = null, Cancellat Payload = JsonSerializer.Serialize(value), }; - var client = new Batch.BatchClient(_channelFactory.RemoteChannel); + Batch.BatchClient client = new(_channelFactory.RemoteChannel); EnqueueResponse response = await client.EnqueueAsync(request, null, null, token); return Guid.Parse(response.TaskId); } /// - /// Job processing status. + /// Batch service processing status. /// - /// Canellation token. + /// Cancellation token. public async Task StatusAsync(CancellationToken token = default) { - var client = new Batch.BatchClient(_channelFactory.RemoteChannel); + Batch.BatchClient client = new(_channelFactory.RemoteChannel); StatusResponse response = await client.StatusAsync(new(), null, null, token); return new() @@ -64,10 +64,7 @@ public async Task StatusAsync(CancellationToken token = d /// /// Test the batch service backend. /// - public async Task TestService() - { - await StatusAsync(); - } + public async Task TestService() => await StatusAsync(); } } #pragma warning restore CA1812 // Internal class is never instantiated diff --git a/src/FunderMaps.Infrastructure/BatchClient/BatchOptions.cs b/src/FunderMaps.Infrastructure/BatchClient/BatchOptions.cs index 0e2666632..62028c3b4 100644 --- a/src/FunderMaps.Infrastructure/BatchClient/BatchOptions.cs +++ b/src/FunderMaps.Infrastructure/BatchClient/BatchOptions.cs @@ -5,16 +5,16 @@ namespace FunderMaps.Infrastructure.BatchClient /// /// Options for the batch service. /// - public sealed class BatchOptions + public sealed record BatchOptions { /// /// Base service uri for batch service. /// - public Uri ServiceUri { get; set; } + public Uri ServiceUri { get; init; } = new("http://localhost"); /// /// Name of the blob storage. /// - public bool TlsValidate { get; set; } + public bool TlsValidate { get; set; } = true; } } diff --git a/src/FunderMaps.Infrastructure/BatchClient/ChannelFactory.cs b/src/FunderMaps.Infrastructure/BatchClient/ChannelFactory.cs index 596db08c9..9a200ee5c 100644 --- a/src/FunderMaps.Infrastructure/BatchClient/ChannelFactory.cs +++ b/src/FunderMaps.Infrastructure/BatchClient/ChannelFactory.cs @@ -25,9 +25,15 @@ public ChannelFactory(IOptions options, ILoggerFactory loggerFacto { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); - // NOTE: The httpHandler is disposed when the channel is disposed. + // NOTE: The httpHandler is disposed when the GrpcChannel is disposed. HttpClientHandler httpHandler = new(); + // There is no TLS when connecting over HTTP, disable TLS verification. + if (_options.ServiceUri.Scheme == "http") + { + _options.TlsValidate = false; + } + if (!_options.TlsValidate) { httpHandler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; diff --git a/src/FunderMaps.Infrastructure/Email/EmailService.cs b/src/FunderMaps.Infrastructure/Email/EmailService.cs index bd0fe6e81..69c86ceac 100644 --- a/src/FunderMaps.Infrastructure/Email/EmailService.cs +++ b/src/FunderMaps.Infrastructure/Email/EmailService.cs @@ -13,20 +13,29 @@ namespace FunderMaps.Infrastructure.Email { // FUTURE: Catch ex. - // FUTURE: Turn this into a factory // FUTURE: We can configure much more and we will in a next release. // TODO: Check input. - internal class EmailService : IEmailService, IDisposable + /// + /// Send email to the MTA. + /// + /// + /// Keep the connection to the remote host succinct. MTA's are + /// typically not prepared to deal with long living connections. + /// Also its unclear if is designed + /// to reuse transport connections. + /// + internal class EmailService : IEmailService { private readonly EmailOptions _options; - private readonly ISmtpClient emailClient = new SmtpClient(); - private bool disposedValue; /// /// Logger. /// public ILogger Logger { get; } + /// + /// Create new instance. + /// public EmailService(IOptions options, ILogger logger) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); @@ -85,10 +94,12 @@ public async Task SendAsync(EmailMessage emailMessage) Logger.LogDebug($"Message prepared, try sending message to MTA"); - await emailClient.ConnectAsync(_options.SmtpServer, _options.SmtpPort, _options.SmtpTls); - await emailClient.AuthenticateAsync(_options.SmtpUsername, _options.SmtpPassword); - await emailClient.SendAsync(message); - await emailClient.DisconnectAsync(quit: true); + using SmtpClient client = new(); + + await client.ConnectAsync(_options.SmtpServer, _options.SmtpPort, _options.SmtpTls); + await client.AuthenticateAsync(_options.SmtpUsername, _options.SmtpPassword); + await client.SendAsync(message); + await client.DisconnectAsync(quit: true); Logger.LogInformation($"Message sent with success"); } @@ -99,36 +110,13 @@ public async Task SendAsync(EmailMessage emailMessage) /// public async Task TestService() { - await emailClient.ConnectAsync(_options.SmtpServer, _options.SmtpPort, _options.SmtpTls); - await emailClient.AuthenticateAsync(_options.SmtpUsername, _options.SmtpPassword); - await emailClient.DisconnectAsync(quit: true); - } - - #region Disposable Pattern - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - emailClient.Dispose(); - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - disposedValue = true; - } - } + using SmtpClient client = new(); - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); + await client.ConnectAsync(_options.SmtpServer, _options.SmtpPort, _options.SmtpTls); + await client.AuthenticateAsync(_options.SmtpUsername, _options.SmtpPassword); + await client.NoOpAsync(); + await client.DisconnectAsync(quit: true); } - - #endregion Disposable Pattern } } #pragma warning restore CA1812 // Internal class is never instantiated diff --git a/src/FunderMaps.Infrastructure/FunderMaps.Infrastructure.csproj b/src/FunderMaps.Infrastructure/FunderMaps.Infrastructure.csproj index 859eb3f74..f9fa0e5a1 100644 --- a/src/FunderMaps.Infrastructure/FunderMaps.Infrastructure.csproj +++ b/src/FunderMaps.Infrastructure/FunderMaps.Infrastructure.csproj @@ -18,13 +18,13 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + diff --git a/src/FunderMaps.WebApi/Controllers/Application/ReviewerController.cs b/src/FunderMaps.WebApi/Controllers/Application/ReviewerController.cs index fb49f2fc1..8e0282bd6 100644 --- a/src/FunderMaps.WebApi/Controllers/Application/ReviewerController.cs +++ b/src/FunderMaps.WebApi/Controllers/Application/ReviewerController.cs @@ -44,7 +44,7 @@ public ReviewerController(IMapper mapper, Core.AppContext appContext, IOrganizat /// Return all reviewers. /// /// Cache response for 1 hour. - [HttpGet("reviewer"), ResponseCache(Duration = 60 * 60)] + [HttpGet("reviewer")] public async Task GetAllAsync([FromQuery] PaginationDto pagination) { // Act. diff --git a/tests/FunderMaps.Core.Tests/Entities/BuildingTests.cs b/tests/FunderMaps.Core.Tests/Entities/BuildingTests.cs index a752190aa..5948f75ed 100644 --- a/tests/FunderMaps.Core.Tests/Entities/BuildingTests.cs +++ b/tests/FunderMaps.Core.Tests/Entities/BuildingTests.cs @@ -20,7 +20,7 @@ public void ValidateBuilding() Geometry = "7b2274797065223a224d756c7469506f6c79676f6e222c22636f6f7264696e61746573223a5b5b5b5b352e3833333038343731312c35332e3039333332373638385d2c5b352e3833333033393531392c35332e3039333331343130385d2c5b352e3833333031393439362c35332e30393333333832325d2c5b352e3833323938353930392c35332e3039333332383132385d2c5b352e3833333036303236382c35332e3039333233383438355d2c5b352e38333331333930362c35332e3039333236323136365d2c5b352e3833333038343731312c35332e3039333332373638385d5d5d5d7d", ExternalId = "external-1", ExternalSource = ExternalDataSource.NlBag, - BuildingType = BuildingType.HouseBoat, + BuildingType = BuildingType.Houseboat, NeighborhoodId = "gfm-neighborhood", }; var entity2 = new Building diff --git a/tests/FunderMaps.Core.Tests/FunderMaps.Core.Tests.csproj b/tests/FunderMaps.Core.Tests/FunderMaps.Core.Tests.csproj index e5f0fb72f..c74baa5dd 100644 --- a/tests/FunderMaps.Core.Tests/FunderMaps.Core.Tests.csproj +++ b/tests/FunderMaps.Core.Tests/FunderMaps.Core.Tests.csproj @@ -8,7 +8,7 @@ - + all diff --git a/tests/FunderMaps.IntegrationTests/Backend/Application/ReviewerTests.cs b/tests/FunderMaps.IntegrationTests/Backend/Application/ReviewerTests.cs index 26b50cb3f..633697245 100644 --- a/tests/FunderMaps.IntegrationTests/Backend/Application/ReviewerTests.cs +++ b/tests/FunderMaps.IntegrationTests/Backend/Application/ReviewerTests.cs @@ -92,8 +92,6 @@ public async Task GetAllReviewerReturnAllReviewer(OrganizationRole role, int exp // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(expectedCount, returnList.Count); - Assert.True(response.Headers.CacheControl.Public); - Assert.NotNull(response.Headers.CacheControl.MaxAge); } } } diff --git a/tests/FunderMaps.IntegrationTests/FunderMaps.IntegrationTests.csproj b/tests/FunderMaps.IntegrationTests/FunderMaps.IntegrationTests.csproj index 8a76c114a..dc50ea511 100644 --- a/tests/FunderMaps.IntegrationTests/FunderMaps.IntegrationTests.csproj +++ b/tests/FunderMaps.IntegrationTests/FunderMaps.IntegrationTests.csproj @@ -8,7 +8,7 @@ - +