diff --git a/Directory.Packages.props b/Directory.Packages.props index 2736ab7..a42230b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,8 +1,8 @@ true - 9.0.0 - 9.0.20 + 10.0.0 + 10.0.12 @@ -12,11 +12,9 @@ - + - - @@ -32,8 +30,8 @@ - - + + diff --git a/global.json b/global.json index 394a392..35bdbc7 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "9.0.0", + "version": "10.0.0", "rollForward": "latestFeature", "allowPrerelease": false } -} +} \ No newline at end of file diff --git a/publish/publish-agent.ps1 b/publish/publish-agent.ps1 index 35dd562..edf0776 100644 --- a/publish/publish-agent.ps1 +++ b/publish/publish-agent.ps1 @@ -7,7 +7,7 @@ $buildVersion New-Item -ItemType Directory -Force -Path ".\published" # Remove files/folders inside the published folder Remove-Item ".\published\*" -Recurse -Force -dotnet publish ../src/QF.BySoft.Integration -r win-x64 -p:PublishSingleFile=true /p:Version=$buildVersion -f net9.0 -p:IncludeAllContentForSelfExtract=true -c Release /p:DebugType=none /p:DebugSymbols=false -o ./published/Integration +dotnet publish ../src/QF.BySoft.Integration -r win-x64 -p:PublishSingleFile=true /p:Version=$buildVersion -f net10.0 -p:IncludeAllContentForSelfExtract=true -c Release /p:DebugType=none /p:DebugSymbols=false -o ./published/Integration Copy-Item "../src/QF.BySoft.Integration/appsettings.json" -Destination ".\published\Integration" Copy-Item ".\data" -Destination ".\published\Integration" -Recurse # Copy the manual diff --git a/publish/published-latest-version.txt b/publish/published-latest-version.txt index 9451b7a..41ee19b 100644 --- a/publish/published-latest-version.txt +++ b/publish/published-latest-version.txt @@ -1 +1 @@ -2026.03.25.18195 +2026.10.01.24738 diff --git a/src/QF.BySoft.Entities/QF.BySoft.Entities.csproj b/src/QF.BySoft.Entities/QF.BySoft.Entities.csproj index 1f059d7..4340f62 100644 --- a/src/QF.BySoft.Entities/QF.BySoft.Entities.csproj +++ b/src/QF.BySoft.Entities/QF.BySoft.Entities.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 diff --git a/src/QF.BySoft.Integration/Extensions/DependencyInjectionExtensions.cs b/src/QF.BySoft.Integration/Extensions/DependencyInjectionExtensions.cs index e64be1f..87567d6 100644 --- a/src/QF.BySoft.Integration/Extensions/DependencyInjectionExtensions.cs +++ b/src/QF.BySoft.Integration/Extensions/DependencyInjectionExtensions.cs @@ -7,11 +7,10 @@ public static class DependencyInjectionExtensions { public static void AddFileWatchFeature(this IServiceCollection services) { + // register agent output file created handler + services.AddTransient(); // register agent output file watcher service services.AddHostedService(); - - // register MediatR with current assembly - services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(AgentOutputFileWatcherService).Assembly)); } } diff --git a/src/QF.BySoft.Integration/Features/AgentOutputFile/AgentOutputFileCreated.cs b/src/QF.BySoft.Integration/Features/AgentOutputFile/AgentOutputFileCreated.cs index cea44dd..bc39e4c 100644 --- a/src/QF.BySoft.Integration/Features/AgentOutputFile/AgentOutputFileCreated.cs +++ b/src/QF.BySoft.Integration/Features/AgentOutputFile/AgentOutputFileCreated.cs @@ -1,8 +1,6 @@ -using MediatR; +namespace QF.BySoft.Integration.Features.AgentOutputFile; -namespace QF.BySoft.Integration.Features.AgentOutputFile; - -public class AgentOutputFileCreated : INotification +public class AgentOutputFileCreated { public AgentOutputFileCreated(string filePath) { diff --git a/src/QF.BySoft.Integration/Features/AgentOutputFile/AgentOutputFileCreatedHandler.cs b/src/QF.BySoft.Integration/Features/AgentOutputFile/AgentOutputFileCreatedHandler.cs index effc55a..08a7e36 100644 --- a/src/QF.BySoft.Integration/Features/AgentOutputFile/AgentOutputFileCreatedHandler.cs +++ b/src/QF.BySoft.Integration/Features/AgentOutputFile/AgentOutputFileCreatedHandler.cs @@ -1,12 +1,11 @@ using System.Threading; using System.Threading.Tasks; -using MediatR; using Microsoft.Extensions.Logging; using QF.BySoft.Integration.Features.BySoftIntegration; namespace QF.BySoft.Integration.Features.AgentOutputFile; -public class AgentOutputFileCreatedHandler : INotificationHandler +public class AgentOutputFileCreatedHandler { private readonly IBySoftIntegration _bySoftIntegration; private readonly ILogger _logger; @@ -19,7 +18,7 @@ public AgentOutputFileCreatedHandler( _bySoftIntegration = bySoftIntegration; } - public async Task Handle(AgentOutputFileCreated notification, CancellationToken cancellationToken) + public async Task HandleAsync(AgentOutputFileCreated notification, CancellationToken cancellationToken) { _logger.LogInformation("File created: {FilePath}", notification.FilePath); diff --git a/src/QF.BySoft.Integration/Features/AgentOutputFile/AgentOutputFileWatcherService.cs b/src/QF.BySoft.Integration/Features/AgentOutputFile/AgentOutputFileWatcherService.cs index e5c2d1f..87fd24d 100644 --- a/src/QF.BySoft.Integration/Features/AgentOutputFile/AgentOutputFileWatcherService.cs +++ b/src/QF.BySoft.Integration/Features/AgentOutputFile/AgentOutputFileWatcherService.cs @@ -1,7 +1,8 @@ using System; using System.IO; using System.Threading; -using MediatR; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using QF.BySoft.Entities; @@ -11,20 +12,20 @@ namespace QF.BySoft.Integration.Features.AgentOutputFile; /// /// Service that watches on the output directory of the agent for *.json files -/// Publishes an AgentOutputFileCreated notification if a file is created +/// Calls AgentOutputFileCreatedHandler if a file is created /// public class AgentOutputFileWatcherService : FileWatcherService { private readonly ILogger _logger; - private readonly IMediator _mediator; + private readonly IServiceScopeFactory _serviceScopeFactory; private readonly SemaphoreSlim _semaphore = new(1, 1); public AgentOutputFileWatcherService( - IMediator mediator, + IServiceScopeFactory serviceScopeFactory, IOptions options, ILogger logger) { - _mediator = mediator; + _serviceScopeFactory = serviceScopeFactory; _logger = logger; // add file watcher to the agent output directory @@ -49,7 +50,7 @@ protected override async void OnAllChanges(object sender, FileSystemEventArgs e) switch (e.ChangeType) { case WatcherChangeTypes.Created: - await _mediator.Publish(new AgentOutputFileCreated(e.FullPath)); + await HandleCreatedFileAsync(e.FullPath, CancellationToken.None); break; case WatcherChangeTypes.Deleted: case WatcherChangeTypes.Changed: @@ -85,7 +86,7 @@ protected override async void OnExistingFile(object sender, FileSystemEventArgs case WatcherChangeTypes.Renamed: break; case WatcherChangeTypes.All: - await _mediator.Publish(new AgentOutputFileCreated(e.FullPath)); + await HandleCreatedFileAsync(e.FullPath, CancellationToken.None); break; default: throw new ArgumentOutOfRangeException(); @@ -100,4 +101,11 @@ protected override async void OnExistingFile(object sender, FileSystemEventArgs _semaphore.Release(); } } + + private async Task HandleCreatedFileAsync(string filePath, CancellationToken cancellationToken) + { + using var scope = _serviceScopeFactory.CreateScope(); + var handler = scope.ServiceProvider.GetRequiredService(); + await handler.HandleAsync(new AgentOutputFileCreated(filePath), cancellationToken); + } } diff --git a/src/QF.BySoft.Integration/QF.BySoft.Integration.csproj b/src/QF.BySoft.Integration/QF.BySoft.Integration.csproj index c881e10..af05964 100644 --- a/src/QF.BySoft.Integration/QF.BySoft.Integration.csproj +++ b/src/QF.BySoft.Integration/QF.BySoft.Integration.csproj @@ -2,12 +2,11 @@ Exe - net9.0 + net10.0 eb167adc-6dc9-410b-968d-79e97bdcacb8 - diff --git a/src/QF.BySoft.LocalData/QF.BySoft.LocalData.csproj b/src/QF.BySoft.LocalData/QF.BySoft.LocalData.csproj index a1c5902..56ad2db 100644 --- a/src/QF.BySoft.LocalData/QF.BySoft.LocalData.csproj +++ b/src/QF.BySoft.LocalData/QF.BySoft.LocalData.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 diff --git a/src/QF.BySoft.Manufacturability/QF.BySoft.Manufacturability.csproj b/src/QF.BySoft.Manufacturability/QF.BySoft.Manufacturability.csproj index 5fab5ab..c3d6ba3 100644 --- a/src/QF.BySoft.Manufacturability/QF.BySoft.Manufacturability.csproj +++ b/src/QF.BySoft.Manufacturability/QF.BySoft.Manufacturability.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 diff --git a/src/QF.BySoft.Tests/AgentOutputFileWatcherServiceTests.cs b/src/QF.BySoft.Tests/AgentOutputFileWatcherServiceTests.cs new file mode 100644 index 0000000..8b6afdc --- /dev/null +++ b/src/QF.BySoft.Tests/AgentOutputFileWatcherServiceTests.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using QF.BySoft.Entities; +using QF.BySoft.Integration.Features.AgentOutputFile; +using QF.BySoft.Integration.Features.BySoftIntegration; +using QF.BySoft.Tests.Util; +using Xunit; + +namespace QF.BySoft.Tests; + +public class AgentOutputFileWatcherServiceTests +{ + [Fact] + public async Task OnAllChangesShouldResolveNewBySoftIntegrationPerCreatedFile() + { + var rootDirectory = Path.Combine(Path.GetTempPath(), $"{nameof(AgentOutputFileWatcherServiceTests)}-{Guid.NewGuid():N}"); + Directory.CreateDirectory(rootDirectory); + + var state = new TestState(expectedHandledCount: 2); + var settings = SettingsBuilder.GetBySoftIntegrationSettings(); + settings.RootDirectory = rootDirectory; + + var serviceProvider = BuildServiceProvider(settings, state); + var sut = new TestAgentOutputFileWatcherService( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService>(), + serviceProvider.GetRequiredService>() + ); + + try + { + sut.RaiseAllChanges(new FileSystemEventArgs(WatcherChangeTypes.Created, rootDirectory, "first.json")); + sut.RaiseAllChanges(new FileSystemEventArgs(WatcherChangeTypes.Created, rootDirectory, "second.json")); + + await state.AllHandled.Task.WaitAsync(TimeSpan.FromSeconds(10)); + + state.HandledCount.Should().Be(2); + state.ResolutionCount.Should().Be(2, "a fresh scoped handler dependency graph should be resolved for each event"); + } + finally + { + await sut.StopAsync(CancellationToken.None); + await serviceProvider.DisposeAsync(); + Directory.Delete(rootDirectory, true); + } + } + + private static ServiceProvider BuildServiceProvider(BySoftIntegrationSettings settings, TestState state) + { + var services = new ServiceCollection(); + services.AddSingleton>(Options.Create(settings)); + services.AddSingleton(Mock.Of>()); + services.AddSingleton(Mock.Of>()); + services.AddTransient(_ => + { + Interlocked.Increment(ref state.ResolutionCount); + var bySoftIntegrationMock = new Mock(); + bySoftIntegrationMock + .Setup(x => x.HandleManufacturabilityCheckRequestAsync(It.IsAny())) + .Returns(() => + { + if (Interlocked.Increment(ref state.HandledCount) == state.ExpectedHandledCount) + { + state.AllHandled.TrySetResult(true); + } + + return Task.CompletedTask; + }); + return bySoftIntegrationMock.Object; + }); + services.AddTransient(); + + return services.BuildServiceProvider(); + } + + private sealed class TestAgentOutputFileWatcherService : AgentOutputFileWatcherService + { + public TestAgentOutputFileWatcherService( + IServiceScopeFactory serviceScopeFactory, + IOptions options, + ILogger logger) + : base(serviceScopeFactory, options, logger) + { + } + + public void RaiseAllChanges(FileSystemEventArgs fileSystemEventArgs) + { + OnAllChanges(this, fileSystemEventArgs); + } + } + + private sealed class TestState(int expectedHandledCount) + { + public TaskCompletionSource AllHandled { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + public int ExpectedHandledCount { get; } = expectedHandledCount; + public int HandledCount; + public int ResolutionCount; + } +} + + diff --git a/src/QF.BySoft.Tests/QF.BySoft.Tests.csproj b/src/QF.BySoft.Tests/QF.BySoft.Tests.csproj index 7124111..d610e05 100644 --- a/src/QF.BySoft.Tests/QF.BySoft.Tests.csproj +++ b/src/QF.BySoft.Tests/QF.BySoft.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 false $(NoWarn);VSTHRD200