Skip to content

Commit 5898a5d

Browse files
committed
Merge branch 'master' into develop
2 parents 52be862 + 1077a50 commit 5898a5d

7 files changed

Lines changed: 105 additions & 25 deletions

File tree

src/Cortside.Common.Hosting.Tests/Cortside.Common.Hosting.Tests.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFramework>net8.0</TargetFramework>
44
<IsPackable>false</IsPackable>
@@ -8,6 +8,9 @@
88
<PrivateAssets>all</PrivateAssets>
99
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1010
</PackageReference>
11+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
12+
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.3" />
13+
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.3" />
1114
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
1215
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.13.2">
1316
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -18,6 +21,7 @@
1821
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1922
<PrivateAssets>all</PrivateAssets>
2023
</PackageReference>
24+
<PackageReference Include="Shouldly" Version="4.3.0" />
2125
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.7.0.110445">
2226
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2327
<PrivateAssets>all</PrivateAssets>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Cortside.Common.Hosting.Tests.Hosting {
2+
public class TestTimedHostedConfiguration {
3+
public int Interval { get; set; }
4+
public bool Enabled { get; set; }
5+
}
6+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace Cortside.Common.Hosting.Tests.Hosting {
6+
public class TestTimedHostedService : TimedHostedService, IMonitoredHostedService {
7+
public TestTimedHostedService(ILogger logger, TestTimedHostedConfiguration config) : base(logger, config.Enabled, config.Interval) {
8+
}
9+
10+
public Task PublicExecuteAsync(CancellationToken stoppingToken) {
11+
return base.ExecuteAsync(stoppingToken);
12+
}
13+
14+
protected override Task ExecuteIntervalAsync() {
15+
Executed = true;
16+
return Task.CompletedTask;
17+
}
18+
19+
public bool Executed { get; set; } = false;
20+
}
21+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Linq;
2+
using Cortside.Common.Hosting.Tests.Hosting;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Extensions.Logging;
6+
using Moq;
7+
using Shouldly;
8+
using Xunit;
9+
10+
namespace Cortside.Common.Hosting.Tests {
11+
public class MonitoredHostedServiceTest {
12+
[Fact]
13+
public void ShouldResolveAllMonitoredServices() {
14+
var services = new ServiceCollection();
15+
var logger = new Mock<ILogger>();
16+
services.AddSingleton(logger.Object);
17+
services.AddHostedService<TestTimedHostedService>();
18+
services.AddSingleton(new TestTimedHostedConfiguration() { Enabled = true, Interval = 30 });
19+
var serviceProvider = services.BuildServiceProvider();
20+
21+
var monitoredServices = serviceProvider.GetServices<IHostedService>()
22+
.Where(x =>
23+
x.GetType().GetInterfaces().Any(y => y == typeof(IMonitoredHostedService))
24+
)
25+
.ToList();
26+
monitoredServices.ShouldNotBeNull();
27+
monitoredServices.ShouldNotBeEmpty();
28+
monitoredServices.ShouldContain(x => x.GetType() == typeof(TestTimedHostedService));
29+
30+
var monitoredService = monitoredServices.First(x => x.GetType().GetInterfaces().Any(y => y == typeof(IMonitoredHostedService))) as IMonitoredHostedService;
31+
monitoredService.ShouldNotBeNull();
32+
monitoredService.Interval.TotalSeconds.ShouldBe(30);
33+
monitoredService.LastActivity.ShouldBeInRange(System.DateTime.UtcNow.AddSeconds(-1), System.DateTime.UtcNow.AddSeconds(1));
34+
}
35+
}
36+
}

src/Cortside.Common.Hosting.Tests/TimedHostedServiceTests.cs renamed to src/Cortside.Common.Hosting.Tests/TimedHostedServiceTest.cs

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,24 @@
11
using System;
22
using System.Threading;
33
using System.Threading.Tasks;
4+
using Cortside.Common.Hosting.Tests.Hosting;
45
using Microsoft.Extensions.Logging;
56
using Moq;
67
using Xunit;
78

89
namespace Cortside.Common.Hosting.Tests {
9-
public class TimedHostedServiceTests {
10-
private class TestTimedHostedService : TimedHostedService {
11-
public TestTimedHostedService(ILogger logger, bool enabled, int interval) : base(logger, enabled, interval) {
12-
}
13-
14-
public Task PublicExecuteAsync(CancellationToken stoppingToken) {
15-
return base.ExecuteAsync(stoppingToken);
16-
}
17-
18-
protected override Task ExecuteIntervalAsync() {
19-
Executed = true;
20-
return Task.CompletedTask;
21-
}
22-
23-
public bool Executed { get; set; } = false;
24-
}
25-
10+
public class TimedHostedServiceTest {
2611
private readonly TestTimedHostedService instance;
2712
private readonly Mock<ILogger> logger;
2813

29-
public TimedHostedServiceTests() {
14+
public TimedHostedServiceTest() {
3015
logger = new Mock<ILogger>();
31-
instance = new TestTimedHostedService(logger.Object, true, 500);
16+
instance = new TestTimedHostedService(logger.Object, new TestTimedHostedConfiguration() { Enabled = true, Interval = 500 });
3217
}
3318

3419
[Fact(Skip = "probably a good check to add")]
3520
public void CannotConstructWithNullLogger() {
36-
Assert.Throws<ArgumentNullException>(() => new TestTimedHostedService(default, true, 5000));
21+
Assert.Throws<ArgumentNullException>(() => new TestTimedHostedService(default, new TestTimedHostedConfiguration() { Enabled = true, Interval = 5000 }));
3722
}
3823

3924
[Fact]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
namespace Cortside.Common.Hosting {
4+
public interface IMonitoredHostedService {
5+
/// <summary>
6+
/// Last activity time for purposes of being able to monitor health
7+
/// </summary>
8+
DateTime LastActivity { get; }
9+
10+
/// <summary>
11+
/// Sleep delay/interval between executions
12+
/// </summary>
13+
TimeSpan Interval { get; }
14+
}
15+
}

src/Cortside.Common.Hosting/TimedHostedService.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Threading;
44
using System.Threading.Tasks;
@@ -15,6 +15,7 @@ public abstract class TimedHostedService : BackgroundService {
1515
private readonly int interval;
1616
private readonly bool enabled;
1717
private readonly bool generateCorrelationId;
18+
protected DateTime lastActivity = DateTime.UtcNow;
1819

1920
/// <summary>
2021
/// Initializes new instance of the Hosted Service
@@ -28,17 +29,19 @@ protected TimedHostedService(ILogger logger, bool enabled, int interval, bool ge
2829

2930
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
3031
// force async so that hosted service does not block Startup
31-
#pragma warning disable _MissingConfigureAwait // Consider using .ConfigureAwait(false).
3232
await Task.Yield();
33-
#pragma warning restore _MissingConfigureAwait // Consider using .ConfigureAwait(false).
3433

3534
if (enabled) {
3635
logger.LogInformation($"{this.GetType().Name} is starting with interval of {interval} seconds");
37-
3836
stoppingToken.Register(() => logger.LogDebug($"{this.GetType().Name} is stopping."));
3937

4038
while (!stoppingToken.IsCancellationRequested) {
39+
// last execution set before and after interval to catch start and catch after sleep delay
40+
lastActivity = DateTime.UtcNow;
41+
4142
await IntervalAsync();
43+
lastActivity = DateTime.UtcNow;
44+
4245
await Task.Delay(TimeSpan.FromSeconds(interval), stoppingToken).ConfigureAwait(false);
4346
}
4447
logger.LogInformation($"{this.GetType().Name} is stopping");
@@ -61,5 +64,15 @@ private async Task IntervalAsync() {
6164
}
6265

6366
protected abstract Task ExecuteIntervalAsync();
67+
68+
/// <summary>
69+
/// Last activity time for purposes of being able to monitor health
70+
/// </summary>
71+
public DateTime LastActivity => lastActivity;
72+
73+
/// <summary>
74+
/// Sleep delay/interval between executions
75+
/// </summary>
76+
public TimeSpan Interval => TimeSpan.FromSeconds(interval);
6477
}
6578
}

0 commit comments

Comments
 (0)