Skip to content

Commit ac031eb

Browse files
authored
Refactor startup sequence (#8)
* Refactor startup builder extensions * Fix dependency injection * Fine tune startup extensions
1 parent 463e14c commit ac031eb

File tree

15 files changed

+155
-114
lines changed

15 files changed

+155
-114
lines changed

src/Contracts/ITriggerMapper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace InvvardDev.Ifttt.Contracts;
22

33
public interface ITriggerMapper
44
{
5-
Task<ITriggerMapper> MapTriggerProcessors();
5+
Task MapTriggerProcessors(CancellationToken stoppingToken);
66

7-
Task<ITriggerMapper> MapTriggerFields();
7+
Task MapTriggerFields(CancellationToken stoppingToken);
88
}

src/Controllers/TestSetupController.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ public async Task<IActionResult> SetupTest()
2828
{
2929
var processors = await testSetup.PrepareSetupListing();
3030

31-
var payload = TopLevelMessageModel<SamplesPayload>.Serialize(new SamplesPayload(processors));
31+
var samples = new SamplesPayload(processors);
32+
samples.SkimEmptyProcessors();
3233

34+
var payload = new TopLevelMessageModel<SamplesPayload>(samples);
35+
3336
return Ok(payload);
3437
}
3538
catch (Exception ex)

src/Controllers/TriggerController.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using InvvardDev.Ifttt.Contracts;
22
using InvvardDev.Ifttt.Hosting.Models;
3+
using InvvardDev.Ifttt.Models.Core;
34
using InvvardDev.Ifttt.Toolkit.Contracts;
45
using InvvardDev.Ifttt.Toolkit.Models;
56
using Microsoft.AspNetCore.Mvc;
@@ -8,7 +9,7 @@ namespace InvvardDev.Ifttt.Controllers;
89

910
[ApiController]
1011
[Route(IftttConstants.BaseTriggersApiPath)]
11-
public class TriggerController(IProcessorService triggerService) : ControllerBase
12+
public class TriggerController([FromKeyedServices(ProcessorKind.Trigger)] IProcessorService triggerService) : ControllerBase
1213
{
1314
[HttpPost("{triggerSlug}")]
1415
public async Task<IActionResult> ExecuteTrigger(string triggerSlug, TriggerRequest triggerRequest)
@@ -19,7 +20,7 @@ public async Task<IActionResult> ExecuteTrigger(string triggerSlug, TriggerReque
1920
}
2021

2122
await trigger.ExecuteAsync(triggerRequest);
22-
23+
2324
return Ok();
2425
}
2526
}
Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
namespace InvvardDev.Ifttt.Hosting;
22

3-
public sealed class DefaultIftttServiceBuilder(IServiceCollection services, string? serviceKey, string realTimeBaseAddress) : IIftttServiceBuilder
3+
public sealed class DefaultIftttServiceBuilder(IServiceCollection services) : IIftttServiceBuilder
44
{
55
public IServiceCollection Services { get; } = services;
66

7-
public string? ServiceKey { get; set; } = serviceKey;
8-
9-
public string RealTimeBaseAddress { get; } = realTimeBaseAddress;
107
}

src/Hosting/Builders/IIftttServiceBuilder.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,4 @@
33
public interface IIftttServiceBuilder
44
{
55
IServiceCollection Services { get; }
6-
7-
string? ServiceKey { get; set; }
8-
9-
string RealTimeBaseAddress { get; }
106
}

src/Hosting/IftttServiceHostingExtensions.cs

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,67 +10,63 @@ namespace InvvardDev.Ifttt.Hosting;
1010

1111
public static class IftttServiceHostingExtensions
1212
{
13-
public static IWebHostBuilder AddIftttToolkit(this IWebHostBuilder hostBuilder,
14-
Action<IIftttServiceBuilder, IftttOptions> configureServicesDelegate)
13+
public static IIftttServiceBuilder AddIftttToolkit(this IServiceCollection services, string serviceKey)
1514
{
16-
ArgumentNullException.ThrowIfNull(hostBuilder);
17-
ArgumentNullException.ThrowIfNull(configureServicesDelegate);
18-
19-
return hostBuilder.ConfigureServices((ctx, services) =>
20-
{
21-
var options = new IftttOptions();
22-
configureServicesDelegate(AddIftttToolkit(services, options), options);
23-
});
15+
ArgumentNullException.ThrowIfNull(services);
16+
ArgumentNullException.ThrowIfNull(serviceKey);
17+
18+
return services.AddIftttToolkit(options => options.ServiceKey = serviceKey);
2419
}
2520

26-
public static IWebHostBuilder ConfigureIftttToolkit(this IWebHostBuilder hostBuilder,
27-
Action<IIftttAppBuilder> configureAppDelegate)
21+
public static IIftttServiceBuilder AddIftttToolkit(this IServiceCollection services, Action<IftttOptions> setupAction)
2822
{
29-
ArgumentNullException.ThrowIfNull(hostBuilder);
30-
ArgumentNullException.ThrowIfNull(configureAppDelegate);
31-
32-
return hostBuilder.Configure((context, applicationBuilder) =>
33-
{
34-
configureAppDelegate(new DefaultIftttAppBuilder(applicationBuilder));
35-
});
23+
ArgumentNullException.ThrowIfNull(services);
24+
ArgumentNullException.ThrowIfNull(setupAction);
25+
26+
var builder = new DefaultIftttServiceBuilder(services);
27+
28+
builder.Services.Configure(setupAction);
29+
30+
return AddIftttToolkitCore(builder);
3631
}
3732

38-
public static IIftttServiceBuilder UseServiceKeyAuthentication(this IIftttServiceBuilder builder, string serviceKey)
33+
public static IIftttServiceBuilder AddTestSetupService<T>(this IIftttServiceBuilder builder)
34+
where T : class, ITestSetup
3935
{
4036
ArgumentNullException.ThrowIfNull(builder);
41-
ArgumentNullException.ThrowIfNull(serviceKey);
4237

43-
builder.ServiceKey = serviceKey;
38+
builder.Services.AddScoped<ITestSetup, T>();
4439

4540
return builder;
4641
}
4742

48-
public static IIftttAppBuilder UseAuthentication(this IIftttAppBuilder appBuilder)
43+
private static IIftttServiceBuilder AddIftttToolkitCore(IIftttServiceBuilder builder)
4944
{
50-
ArgumentNullException.ThrowIfNull(appBuilder);
45+
builder.Services
46+
.AddControllers()
47+
.AddApplicationPart(Assembly.GetAssembly(typeof(StatusController)) ?? throw new InvalidOperationException())
48+
.AddControllersAsServices();
5149

52-
appBuilder.App.UseMiddleware<ServiceKeyMiddleware>();
50+
builder.Services
51+
.AddScoped<IAssemblyAccessor, AssemblyAccessor>()
52+
.AddSingleton<IProcessorRepository, ProcessorRepository>();
5353

54-
return appBuilder;
54+
return builder;
5555
}
5656

57-
private static IIftttServiceBuilder AddIftttToolkit(IServiceCollection services, IftttOptions options)
57+
public static IIftttAppBuilder ConfigureIftttToolkit(this IApplicationBuilder appBuilder)
5858
{
59-
if (Uri.IsWellFormedUriString(options.RealTimeBaseAddress, UriKind.RelativeOrAbsolute))
60-
{
61-
throw new UriFormatException("The RealTimeBaseAddress is not a valid URI.");
62-
}
63-
64-
var builder = new DefaultIftttServiceBuilder(services, options.ServiceKey, options.RealTimeBaseAddress);
59+
ArgumentNullException.ThrowIfNull(appBuilder);
6560

66-
var apiBuilder = builder.Services.AddControllers();
61+
return new DefaultIftttAppBuilder(appBuilder);
62+
}
6763

68-
apiBuilder.AddApplicationPart(Assembly.GetAssembly(typeof(StatusController)) ?? throw new InvalidOperationException())
69-
.AddControllersAsServices();
64+
public static IIftttAppBuilder UseServiceKeyAuthentication(this IIftttAppBuilder appBuilder)
65+
{
66+
ArgumentNullException.ThrowIfNull(appBuilder);
7067

71-
builder.Services.AddScoped<IAssemblyAccessor, AssemblyAccessor>();
72-
builder.Services.AddSingleton<IProcessorRepository, ProcessorRepository>();
68+
appBuilder.App.UseMiddleware<ServiceKeyMiddleware>();
7369

74-
return builder;
70+
return appBuilder;
7571
}
76-
}
72+
}

src/Hosting/Middleware/ServiceKeyMiddleware.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
using InvvardDev.Ifttt.Hosting.Models;
2+
using Microsoft.Extensions.Options;
23

34
namespace InvvardDev.Ifttt.Hosting.Middleware;
45

5-
internal class ServiceKeyMiddleware(RequestDelegate next, string serviceKey)
6+
internal class ServiceKeyMiddleware(RequestDelegate next, IOptions<IftttOptions> options)
67
{
8+
private readonly string serviceKey = options.Value.ServiceKey ?? throw new ArgumentNullException(nameof(options));
9+
710
public async Task InvokeAsync(HttpContext context)
811
{
912
if (context.Request.Headers.TryGetValue(IftttConstants.ServiceKeyHeader, out var receivedServiceKey)

src/Hosting/Models/IftttOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace InvvardDev.Ifttt.Hosting.Models;
22

33
public class IftttOptions
44
{
5-
public string? ServiceKey { get; }
5+
public string? ServiceKey { get; set; }
66

77
/// <summary>
88
/// Gets or sets the base address for the IFTTT Realtime API (default: https://realtime.ifttt.com/v1/notifications/).

src/Hosting/TriggerHostingExtensions.cs

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,59 @@
1-
using System.Reflection;
2-
using InvvardDev.Ifttt.Contracts;
3-
using InvvardDev.Ifttt.Controllers;
1+
using InvvardDev.Ifttt.Contracts;
42
using InvvardDev.Ifttt.Hosting.Models;
53
using InvvardDev.Ifttt.Models.Core;
64
using InvvardDev.Ifttt.Reflection;
75
using InvvardDev.Ifttt.Services;
86
using InvvardDev.Ifttt.Toolkit.Contracts;
97
using InvvardDev.Ifttt.Toolkit.Hooks;
8+
using Microsoft.Extensions.Options;
109

1110
namespace InvvardDev.Ifttt.Hosting;
1211

1312
public static class TriggerHostingExtensions
1413
{
1514
public static IIftttServiceBuilder AddTriggers(this IIftttServiceBuilder builder)
1615
{
17-
builder.Services.AddHttpClient(IftttConstants.TriggerHttpClientName, (_, client) =>
16+
ArgumentNullException.ThrowIfNull(builder);
17+
18+
builder.Services.AddHttpClient(IftttConstants.TriggerHttpClientName, (factory, client) =>
1819
{
19-
client.BaseAddress = new Uri(builder.RealTimeBaseAddress);
20-
client.DefaultRequestHeaders.Add(IftttConstants.ServiceKeyHeader, builder.ServiceKey);
21-
});
20+
var options = factory.GetRequiredService<IOptions<IftttOptions>>();
2221

23-
builder.Services.AddTransient<ITriggerHook, RealTimeNotificationWebHook>();
22+
client.BaseAddress = new Uri(options.Value.RealTimeBaseAddress);
23+
client.DefaultRequestHeaders.Add(IftttConstants.ServiceKeyHeader, options.Value.ServiceKey);
24+
});
2425

2526
builder.Services
26-
.AddControllers()
27-
.AddApplicationPart(Assembly.GetAssembly(typeof(TriggerController)) ?? throw new InvalidOperationException())
28-
.AddControllersAsServices();
27+
.AddKeyedTransient<IProcessorService, TriggerService>(ProcessorKind.Trigger)
28+
.AddKeyedTransient<IAttributeLookup, TriggerAttributeLookup>(nameof(TriggerAttributeLookup))
29+
.AddKeyedTransient<IAttributeLookup, TriggerFieldsAttributeLookup>(nameof(TriggerFieldsAttributeLookup));
30+
31+
builder.Services.AddTransient<ITriggerHook, RealTimeNotificationWebHook>();
2932

3033
return builder;
3134
}
3235

3336
public static IIftttServiceBuilder AddTriggerAutoMapper(this IIftttServiceBuilder builder)
3437
{
38+
ArgumentNullException.ThrowIfNull(builder);
39+
3540
builder.Services
3641
.AddTransient<ITriggerMapper, TriggerMapper>()
37-
.AddKeyedTransient<IProcessorService, TriggerService>(ProcessorKind.Trigger)
38-
.AddKeyedTransient<IAttributeLookup, TriggerAttributeLookup>(nameof(TriggerAttributeLookup))
39-
.AddKeyedTransient<IAttributeLookup, TriggerFieldsAttributeLookup>(nameof(TriggerFieldsAttributeLookup))
4042
.AddHostedService<TriggerAutoMapperService>();
4143

4244
return builder;
4345
}
4446

4547
public static IIftttAppBuilder ConfigureTriggers(this IIftttAppBuilder appBuilder)
4648
{
47-
appBuilder.App.UseEndpoints(endpoints =>
48-
{
49-
endpoints.MapControllers();
50-
});
49+
ArgumentNullException.ThrowIfNull(appBuilder);
50+
51+
appBuilder.App
52+
.UseRouting()
53+
.UseEndpoints(endpoints =>
54+
{
55+
endpoints.MapControllers();
56+
});
5157

5258
return appBuilder;
5359
}

src/Reflection/TriggerMapper.cs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,31 @@ namespace InvvardDev.Ifttt.Reflection;
99
internal class TriggerMapper(
1010
[FromKeyedServices(ProcessorKind.Trigger)] IProcessorService triggerService,
1111
[FromKeyedServices(nameof(TriggerAttributeLookup))] IAttributeLookup triggerAttributeLookup,
12-
[FromKeyedServices(nameof(TriggerFieldsAttributeLookup))] IAttributeLookup triggerFieldsAttributeLookup) : ITriggerMapper
12+
[FromKeyedServices(nameof(TriggerFieldsAttributeLookup))] IAttributeLookup triggerFieldsAttributeLookup,
13+
ILogger<TriggerMapper> logger) : ITriggerMapper
1314
{
14-
public async Task<ITriggerMapper> MapTriggerProcessors()
15+
public async Task MapTriggerProcessors(CancellationToken stoppingToken)
1516
=> await MapAttribute<TriggerAttribute>(triggerAttributeLookup.GetAnnotatedTypes(),
1617
async (triggerSlug, type) => await triggerService.GetProcessor(triggerSlug) switch
1718
{
1819
null => false,
1920
{ } existingProcessorTree when existingProcessorTree.Type == type
2021
=> true,
2122
{ } pt when pt.Type != type
22-
=> throw new InvalidOperationException($"Conflict: 'Trigger' processor with slug '{triggerSlug}' already exists (Type is '{pt.Type}')."),
23+
=> throw new
24+
InvalidOperationException($"Conflict: 'Trigger' processor with slug '{triggerSlug}' already exists (Type is '{pt.Type}')."),
2325
_ => throw new ArgumentOutOfRangeException()
24-
});
26+
},
27+
stoppingToken);
2528

26-
public async Task<ITriggerMapper> MapTriggerFields()
29+
public async Task MapTriggerFields(CancellationToken stoppingToken)
2730
=> await MapAttribute<TriggerFieldsAttribute>(triggerFieldsAttributeLookup.GetAnnotatedTypes(),
2831
async (triggerSlug, _) => await triggerService.Exists(triggerSlug) switch
2932
{
3033
true => true,
3134
false => throw new InvalidOperationException($"There is no trigger with slug '{triggerSlug}' registered.")
32-
});
35+
},
36+
stoppingToken);
3337

3438
private async Task MapTriggerFieldProperties(string parentSlug, Type parentType)
3539
{
@@ -43,19 +47,32 @@ private async Task MapTriggerFieldProperties(string parentSlug, Type parentType)
4347
}
4448
}
4549

46-
private async Task<TriggerMapper> MapAttribute<TAttribute>(IEnumerable<Type> types, Func<string, Type, Task<bool>> processorExists)
50+
private async Task<TriggerMapper> MapAttribute<TAttribute>(IEnumerable<Type> types, Func<string, Type, Task<bool>> processorExists, CancellationToken stoppingToken = default)
4751
where TAttribute : ProcessorAttributeBase
4852
{
49-
foreach (var type in types)
50-
{
51-
if (type.GetCustomAttribute<TAttribute>() is not { } attribute) continue;
53+
stoppingToken.ThrowIfCancellationRequested();
5254

53-
if (await processorExists(attribute.Slug, type) is false)
55+
try
56+
{
57+
foreach (var type in types)
5458
{
55-
await triggerService.AddOrUpdateProcessor(new ProcessorTree(attribute.Slug, type, ProcessorKind.Trigger));
56-
}
59+
if (type.GetCustomAttribute<TAttribute>() is not { } attribute) continue;
5760

58-
await MapTriggerFieldProperties(attribute.Slug, type);
61+
if (await processorExists(attribute.Slug, type) is false)
62+
{
63+
await triggerService.AddOrUpdateProcessor(new ProcessorTree(attribute.Slug, type, ProcessorKind.Trigger));
64+
}
65+
66+
await MapTriggerFieldProperties(attribute.Slug, type);
67+
}
68+
}
69+
catch (OperationCanceledException ocex)
70+
{
71+
logger.LogInformation(ocex, "Mapping for attribute '{AttributeType}' is cancelled.", typeof(TAttribute));
72+
}
73+
catch (Exception ex)
74+
{
75+
logger.LogError(ex, "Mapping for attribute '{AttributeType}' is failed.", typeof(TAttribute));
5976
}
6077

6178
return this;

0 commit comments

Comments
 (0)