Skip to content

Commit 9789e42

Browse files
committed
Adding implementation for Azure Functions isolated worker model extension
1 parent 64dacb9 commit 9789e42

9 files changed

+260
-0
lines changed

Lib.Net.Http.WebPush.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
2929
README.md = README.md
3030
EndProjectSection
3131
EndProject
32+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib.Azure.Functions.Worker.Extensions.WebPush", "src\Lib.Azure.Functions.Worker.Extensions.WebPush\Lib.Azure.Functions.Worker.Extensions.WebPush.csproj", "{FF54B26A-8E32-4D2F-B91E-C8BAAFE9148D}"
33+
EndProject
3234
Global
3335
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3436
Debug|Any CPU = Debug|Any CPU
@@ -59,6 +61,10 @@ Global
5961
{E0746BE4-61F3-4366-8C7E-99C2350A0DD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
6062
{E0746BE4-61F3-4366-8C7E-99C2350A0DD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
6163
{E0746BE4-61F3-4366-8C7E-99C2350A0DD3}.Release|Any CPU.Build.0 = Release|Any CPU
64+
{FF54B26A-8E32-4D2F-B91E-C8BAAFE9148D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
65+
{FF54B26A-8E32-4D2F-B91E-C8BAAFE9148D}.Debug|Any CPU.Build.0 = Debug|Any CPU
66+
{FF54B26A-8E32-4D2F-B91E-C8BAAFE9148D}.Release|Any CPU.ActiveCfg = Release|Any CPU
67+
{FF54B26A-8E32-4D2F-B91E-C8BAAFE9148D}.Release|Any CPU.Build.0 = Release|Any CPU
6268
EndGlobalSection
6369
GlobalSection(SolutionProperties) = preSolution
6470
HideSolutionNode = FALSE
@@ -70,6 +76,7 @@ Global
7076
{001A4013-EE58-428A-BD6C-49AC3A0CA6F9} = {0441724F-48F7-4863-BBA6-586E10182605}
7177
{D9CF3828-E0F8-4A92-97D3-4B79E9C38F58} = {0441724F-48F7-4863-BBA6-586E10182605}
7278
{E0746BE4-61F3-4366-8C7E-99C2350A0DD3} = {AFD9AC51-E923-49DF-B000-721816ABDE37}
79+
{FF54B26A-8E32-4D2F-B91E-C8BAAFE9148D} = {0441724F-48F7-4863-BBA6-586E10182605}
7380
EndGlobalSection
7481
GlobalSection(ExtensibilityGlobals) = postSolution
7582
SolutionGuid = {88535C7E-5564-4AC1-A41B-18DD05637943}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Lib.Azure.Functions.Worker.Extensions.WebPush
2+
{
3+
internal static class Constants
4+
{
5+
internal const string PUSH_SERVICE_EXTENSION_NAME = "PushService";
6+
7+
internal const string JSON_CONTENT_TYPE = "application/json";
8+
}
9+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using Microsoft.Azure.Functions.Worker;
3+
using Microsoft.Extensions.DependencyInjection;
4+
5+
namespace Lib.Azure.Functions.Worker.Extensions.WebPush
6+
{
7+
internal static class FunctionsWorkerApplicationBuilderExtensions
8+
{
9+
public static IFunctionsWorkerApplicationBuilder ConfigurePushServiceExtension(this IFunctionsWorkerApplicationBuilder builder)
10+
{
11+
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
12+
13+
builder.Services.AddHttpClient();
14+
15+
return builder;
16+
}
17+
}
18+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<Description>This package contains Azure Functions isolated worker model extensions for Web Push Protocol based client for Push Service.</Description>
4+
<Tags>Azure AzureFunctions IsolatedWorker WebPush</Tags>
5+
<Copyright>Copyright © 2024 Tomasz Pęczek</Copyright>
6+
<VersionPrefix>1.0.0</VersionPrefix>
7+
<Authors>Tomasz Pęczek</Authors>
8+
<TargetFramework>net6.0</TargetFramework>
9+
<AssemblyTitle>Lib.Azure.Functions.Worker.Extensions.WebPush</AssemblyTitle>
10+
<AssemblyName>Lib.Azure.Functions.Worker.Extensions.WebPush</AssemblyName>
11+
<PackageId>Lib.Azure.Functions.Worker.Extensions.WebPush</PackageId>
12+
<PackageTags>azure;azurefunctions;isolatedworker;webpush</PackageTags>
13+
<PackageProjectUrl>https://github.com/tpeczek/Lib.Net.Http.WebPush</PackageProjectUrl>
14+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
15+
<PackageReadmeFile>README.md</PackageReadmeFile>
16+
<RepositoryType>git</RepositoryType>
17+
<RepositoryUrl>git://github.com/tpeczek/Lib.Net.Http.WebPush</RepositoryUrl>
18+
<GenerateAssemblyTitleAttribute>true</GenerateAssemblyTitleAttribute>
19+
<GenerateAssemblyDescriptionAttribute>true</GenerateAssemblyDescriptionAttribute>
20+
<GenerateAssemblyProductAttribute>true</GenerateAssemblyProductAttribute>
21+
<GenerateAssemblyCopyrightAttribute>true</GenerateAssemblyCopyrightAttribute>
22+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
23+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
24+
</PropertyGroup>
25+
<ItemGroup>
26+
<None Include="..\..\README.md" Pack="true" PackagePath="\"/>
27+
</ItemGroup>
28+
<ItemGroup>
29+
<PackageReference Include="Microsoft.Extensions.Http" Version="[6.0.0,)" />
30+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Core" Version="[1.18.0,)" />
31+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Abstractions" Version="[1.3.0,)" />
32+
</ItemGroup>
33+
<ItemGroup>
34+
<PackageReference Include="Lib.Net.Http.WebPush" Version="3.2.1" />
35+
</ItemGroup>
36+
<ItemGroup>
37+
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.2.4" PrivateAssets="All" />
38+
</ItemGroup>
39+
</Project>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[assembly: Microsoft.Azure.Functions.Worker.Extensions.Abstractions.ExtensionInformation("Lib.Azure.WebJobs.Extensions.WebPush", "1.5.0")]
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using System;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
using Microsoft.Azure.Functions.Worker.Core;
5+
using Microsoft.Azure.Functions.Worker.Converters;
6+
using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;
7+
using Lib.Net.Http.WebPush;
8+
using Lib.Net.Http.WebPush.Authentication;
9+
10+
namespace Lib.Azure.Functions.Worker.Extensions.WebPush
11+
{
12+
[SupportsDeferredBinding]
13+
[SupportedTargetType(typeof(PushServiceClient))]
14+
internal class PushServiceConverter : IInputConverter
15+
{
16+
private static readonly Type TYPE_OF_PUSH_SERVICE_CLIENT = typeof(PushServiceClient);
17+
18+
private readonly IHttpClientFactory _httpClientFactory;
19+
20+
public PushServiceConverter(IHttpClientFactory httpClientFactory)
21+
{
22+
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
23+
}
24+
25+
public ValueTask<ConversionResult> ConvertAsync(ConverterContext context)
26+
{
27+
try
28+
{
29+
if (!CanConvert(context))
30+
{
31+
return new(ConversionResult.Unhandled());
32+
}
33+
34+
var modelBindingData = context?.Source as ModelBindingData;
35+
36+
PushServiceClient pushServiceClient = CreatePushServiceClient(modelBindingData);
37+
38+
return new(ConversionResult.Success(pushServiceClient));
39+
}
40+
catch (Exception ex)
41+
{
42+
return new(ConversionResult.Failed(ex));
43+
}
44+
}
45+
46+
private static bool CanConvert(ConverterContext context)
47+
{
48+
ArgumentNullException.ThrowIfNull(context, nameof(context));
49+
50+
if (context.TargetType != TYPE_OF_PUSH_SERVICE_CLIENT)
51+
{
52+
return false;
53+
}
54+
55+
if (context.Source is not ModelBindingData bindingData)
56+
{
57+
return false;
58+
}
59+
60+
if (bindingData.Source is not Constants.PUSH_SERVICE_EXTENSION_NAME)
61+
{
62+
throw new InvalidOperationException($"Unexpected binding source '{bindingData.Source}'. Only '{Constants.PUSH_SERVICE_EXTENSION_NAME}' is supported.");
63+
}
64+
65+
if (bindingData.ContentType is not Constants.JSON_CONTENT_TYPE)
66+
{
67+
throw new InvalidOperationException($"Unexpected content-type '{bindingData.ContentType}'. Only '{Constants.JSON_CONTENT_TYPE}' is supported.");
68+
}
69+
70+
return true;
71+
}
72+
73+
private PushServiceClient CreatePushServiceClient(ModelBindingData bindingData)
74+
{
75+
var pushServiceModelBindingDataContent = bindingData.Content.ToObjectFromJson<PushServiceModelBindingDataContent>();
76+
77+
PushServiceClient pushServiceClient = new PushServiceClient(_httpClientFactory.CreateClient())
78+
{
79+
DefaultAuthentication = new VapidAuthentication(pushServiceModelBindingDataContent.PublicKey, pushServiceModelBindingDataContent.PrivateKey)
80+
{
81+
Subject = pushServiceModelBindingDataContent.Subject
82+
},
83+
AutoRetryAfter = pushServiceModelBindingDataContent.AutoRetryAfter,
84+
MaxRetriesAfter = pushServiceModelBindingDataContent.MaxRetriesAfter
85+
};
86+
87+
if (pushServiceModelBindingDataContent.DefaultTimeToLive.HasValue)
88+
{
89+
pushServiceClient.DefaultTimeToLive = pushServiceModelBindingDataContent.DefaultTimeToLive.Value;
90+
}
91+
92+
return pushServiceClient;
93+
}
94+
}
95+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using Microsoft.Azure.Functions.Worker;
3+
using Microsoft.Azure.Functions.Worker.Core;
4+
5+
[assembly: WorkerExtensionStartup(typeof(Lib.Azure.Functions.Worker.Extensions.WebPush.PushServiceExtensionStartup))]
6+
7+
namespace Lib.Azure.Functions.Worker.Extensions.WebPush
8+
{
9+
/// <summary>
10+
/// Class providing a worker extension startup implementation.
11+
/// </summary>
12+
public class PushServiceExtensionStartup : WorkerExtensionStartup
13+
{
14+
/// <summary>
15+
/// Performs the startup configuration action for Push Service extension.
16+
/// </summary>
17+
/// <param name="applicationBuilder">The <see cref="IFunctionsWorkerApplicationBuilder"/> that can be used to configure the worker extension.</param>
18+
public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder)
19+
{
20+
ArgumentNullException.ThrowIfNull(applicationBuilder, nameof(applicationBuilder));
21+
22+
applicationBuilder.ConfigurePushServiceExtension();
23+
}
24+
}
25+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using Microsoft.Azure.Functions.Worker.Converters;
2+
using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;
3+
4+
namespace Lib.Azure.Functions.Worker.Extensions.WebPush
5+
{
6+
/// <summary>
7+
/// Attribute used to configure a parameter as the input target for the PushService binding.
8+
/// </summary>
9+
/// <remarks>
10+
/// The method parameter type can be one of the following:
11+
/// <list type="bullet">
12+
/// <item><description><see cref="Net.Http.WebPush.PushServiceClient"/></description></item>
13+
/// </list>
14+
/// </remarks>
15+
[InputConverter(typeof(PushServiceConverter))]
16+
[ConverterFallbackBehavior(ConverterFallbackBehavior.Default)]
17+
public sealed class PushServiceInputAttribute : InputBindingAttribute
18+
{
19+
/// <summary>
20+
/// The application server public key.
21+
/// </summary>
22+
public string PublicKeySetting { get; set; }
23+
24+
/// <summary>
25+
/// The application server private key.
26+
/// </summary>
27+
public string PrivateKeySetting { get; set; }
28+
29+
/// <summary>
30+
/// The contact information for the application server.
31+
/// </summary>
32+
public string SubjectSetting { get; set; }
33+
34+
/// <summary>
35+
/// The value indicating if client should automatically attempt to retry in case of 429 Too Many Requests.
36+
/// </summary>
37+
public bool AutoRetryAfter { get; set; } = true;
38+
39+
/// <summary>
40+
/// The value indicating the maximum number of automatic attempts to retry in case of 429 Too Many Requests (&lt;= 0 means unlimited).
41+
/// </summary>
42+
public int MaxRetriesAfter { get; set; } = 0;
43+
44+
/// <summary>
45+
/// The default time (in seconds) for which the message should be retained by push service. It will be used when <see cref="Net.Http.WebPush.PushMessage.TimeToLive"/> is not set.
46+
/// </summary>
47+
public int? DefaultTimeToLive { get; set; }
48+
}
49+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Lib.Azure.Functions.Worker.Extensions.WebPush
2+
{
3+
internal class PushServiceModelBindingDataContent
4+
{
5+
public string PublicKey { get; set; }
6+
7+
public string PrivateKey { get; set; }
8+
9+
public string Subject { get; set; }
10+
11+
public bool AutoRetryAfter { get; set; }
12+
13+
public int MaxRetriesAfter { get; set; }
14+
15+
public int? DefaultTimeToLive { get; set; }
16+
}
17+
}

0 commit comments

Comments
 (0)