Skip to content

Commit ce07c1f

Browse files
authored
feat: add IHttpClientFactory support for FileMaker clients (#417)
1 parent e2cd705 commit ce07c1f

File tree

10 files changed

+726
-35
lines changed

10 files changed

+726
-35
lines changed

README.md

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -83,43 +83,76 @@ public class Model
8383
}
8484
```
8585

86-
### Using IHttpClientFactory
86+
### Using Dependency Injection (Recommended)
8787

88-
Constructors take an `HttpClient` and you can setup the DI pipeline in Startup.cs like so for standard use:
88+
The simplest way to register FMData with dependency injection is using the built-in extension method. This sets up `IHttpClientFactory`, registers `ConnectionInfo`, and configures the client as a singleton (preserving auth token state across requests):
8989

9090
```csharp
91-
services.AddSingleton<FMData.ConnectionInfo>(ci => new FMData.ConnectionInfo
91+
services.AddFMDataRest(conn =>
9292
{
93-
FmsUri = "https://example.com",
94-
Username = "user",
95-
Password = "password",
96-
Database = "FILE_NAME"
93+
conn.FmsUri = "https://example.com";
94+
conn.Database = "FILE_NAME";
95+
conn.Username = "user";
96+
conn.Password = "password";
97+
});
98+
```
99+
100+
This registers both `IFileMakerApiClient` and `IFileMakerRestClient`. You can also pass a callback to configure the underlying `HttpClient` (e.g., to set timeouts):
101+
102+
```csharp
103+
services.AddFMDataRest(
104+
conn =>
105+
{
106+
conn.FmsUri = "https://example.com";
107+
conn.Database = "FILE_NAME";
108+
conn.Username = "user";
109+
conn.Password = "password";
110+
},
111+
httpClient =>
112+
{
113+
httpClient.Timeout = TimeSpan.FromSeconds(30);
114+
});
115+
```
116+
117+
The method returns an `IHttpClientBuilder`, so you can chain additional configuration like retry policies.
118+
119+
For the XML client, use `AddFMDataXml` from the `FMData.Xml` namespace:
120+
121+
```csharp
122+
services.AddFMDataXml(conn =>
123+
{
124+
conn.FmsUri = "https://example.com";
125+
conn.Database = "FILE_NAME";
126+
conn.Username = "user";
127+
conn.Password = "password";
97128
});
98-
services.AddHttpClient<IFileMakerApiClient, FileMakerRestClient>();
99129
```
100130

101-
If you prefer to use a singleton instance of `IFileMakerApiClient` you have to do a little bit more work in startup. This can improve performance if you're making lots of hits to the Data API over a single request to your application:
131+
> **Note:** The `AddFMDataRest` and `AddFMDataXml` extension methods require netstandard2.0 or later. If you are targeting netstandard1.3 or net45, use the manual approach below.
132+
133+
#### Manual Registration
134+
135+
You can also construct the client with an `IHttpClientFactory` directly:
136+
137+
```csharp
138+
var factory = serviceProvider.GetRequiredService<IHttpClientFactory>();
139+
var client = new FileMakerRestClient(factory, connectionInfo);
140+
```
141+
142+
Or use the traditional `HttpClient`-based approach:
102143

103144
```csharp
104-
services.AddHttpClient(); // setup IHttpClientFactory in the DI container
105145
services.AddSingleton<FMData.ConnectionInfo>(ci => new FMData.ConnectionInfo
106146
{
107147
FmsUri = "https://example.com",
108148
Username = "user",
109149
Password = "password",
110150
Database = "FILE_NAME"
111151
});
112-
// Keep the FileMaker client as a singleton for speed
113-
services.AddSingleton<IFileMakerApiClient, FileMakerRestClient>(s => {
114-
var hcf = s.GetRequiredService<IHttpClientFactory>();
115-
var ci = s.GetRequiredService<ConnectionInfo>();
116-
return new FileMakerRestClient(hcf.CreateClient(), ci);
117-
});
152+
services.AddHttpClient<IFileMakerApiClient, FileMakerRestClient>();
118153
```
119154

120-
Behind the scenes, the injected `HttpClient` is kept alive for the lifetime of the FMData client (rest/xml) and reused throughout. This is useful to manage the lifetime of `IFileMakerApiClient` as a singleton, since it stores data about FileMaker Data API tokens and reuses them as much as possible. Simply using `services.AddHttpClient<IFileMakerApiClient, FileMakerRestClient>();` keeps the lifetime of our similar to that of a 'managed `HttpClient`' which works for simple scenarios.
121-
122-
Test both approaches in your solution and use what works.
155+
Behind the scenes, the injected `HttpClient` is kept alive for the lifetime of the FMData client (rest/xml) and reused throughout. The client stores FileMaker Data API tokens and reuses them as much as possible.
123156

124157
### Authentication with FileMaker Cloud
125158

docs/configuration.md

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,75 @@ conn.RestTargetVersion = RestTargetVersion.vLatest;
3636
var client = new FileMakerRestClient(new HttpClient(), conn);
3737
```
3838

39+
Or using `IHttpClientFactory` (available on netstandard2.0+):
40+
41+
```csharp
42+
var client = new FileMakerRestClient(httpClientFactory, conn);
43+
```
44+
45+
When constructed with `IHttpClientFactory`, the client uses factory-managed clients for container downloads, avoiding connection pool exhaustion from creating ad-hoc `HttpClient` instances.
46+
3947
### Using Dependency Injection
4048

41-
#### Standard (Scoped) Lifetime
49+
#### Using Extension Methods (Recommended)
50+
51+
The simplest approach uses the built-in `AddFMDataRest` extension method, which handles all registration in one call:
52+
53+
```csharp
54+
services.AddFMDataRest(conn =>
55+
{
56+
conn.FmsUri = "https://your-server.com";
57+
conn.Database = "YourDatabase";
58+
conn.Username = "admin";
59+
conn.Password = "password";
60+
});
61+
```
62+
63+
This registers:
64+
65+
- `ConnectionInfo` as a singleton
66+
- `IAuthTokenProvider` (using `DefaultAuthTokenProvider`)
67+
- A named `HttpClient` via `IHttpClientFactory`
68+
- `FileMakerRestClient` as a singleton, available as both `IFileMakerApiClient` and `IFileMakerRestClient`
69+
70+
You can optionally configure the underlying `HttpClient`:
71+
72+
```csharp
73+
services.AddFMDataRest(
74+
conn =>
75+
{
76+
conn.FmsUri = "https://your-server.com";
77+
conn.Database = "YourDatabase";
78+
conn.Username = "admin";
79+
conn.Password = "password";
80+
},
81+
httpClient =>
82+
{
83+
httpClient.Timeout = TimeSpan.FromSeconds(30);
84+
});
85+
```
86+
87+
The method returns `IHttpClientBuilder`, which lets you chain additional configuration such as retry policies or custom message handlers.
88+
89+
For the XML/CWP client, use `AddFMDataXml` from the `FMData.Xml` namespace:
90+
91+
```csharp
92+
services.AddFMDataXml(conn =>
93+
{
94+
conn.FmsUri = "https://your-server.com";
95+
conn.Database = "YourDatabase";
96+
conn.Username = "admin";
97+
conn.Password = "password";
98+
});
99+
```
100+
101+
> **Note:** The `AddFMDataRest` and `AddFMDataXml` extension methods require netstandard2.0 or later.
102+
103+
#### Manual Registration
104+
105+
If you prefer manual control or are targeting netstandard1.3/net45, you can wire up registration yourself.
42106

43-
Register `IFileMakerApiClient` with `AddHttpClient` for managed `HttpClient` lifetime:
107+
**Standard (Scoped) Lifetime:**
44108

45109
```csharp
46110
services.AddSingleton<ConnectionInfo>(new ConnectionInfo
@@ -56,7 +120,7 @@ services.AddHttpClient<IFileMakerApiClient, FileMakerRestClient>();
56120

57121
This creates a new `FileMakerRestClient` per scope (typically per HTTP request in ASP.NET Core).
58122

59-
#### Singleton Lifetime
123+
**Singleton Lifetime:**
60124

61125
For better performance when making many Data API calls per request, register as a singleton:
62126

src/FMData.Rest/FMData.Rest.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,8 @@
6363
<ItemGroup Condition="$(TargetFramework) == 'net45'">
6464
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
6565
</ItemGroup>
66+
67+
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' AND '$(TargetFramework)' != 'net45'">
68+
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
69+
</ItemGroup>
6670
</Project>

src/FMData.Rest/FileMakerRestClient.cs

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
using FMData.Rest.Responses;
1313
using Newtonsoft.Json;
1414
using Newtonsoft.Json.Linq;
15+
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
16+
using Microsoft.Extensions.DependencyInjection;
17+
#endif
1518

1619
namespace FMData.Rest
1720
{
@@ -55,7 +58,40 @@ public class FileMakerRestClient : FileMakerApiClientBase, IFileMakerRestClient
5558
private readonly bool _useNewClientForContainers = false;
5659
private readonly string _targetVersion = "v1";
5760

61+
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
62+
private readonly IHttpClientFactory _httpClientFactory;
63+
#endif
64+
5865
#region Constructors
66+
67+
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
68+
/// <summary>
69+
/// FM Data Constructor with IHttpClientFactory and ConnectionInfo.
70+
/// Uses the factory to create the primary HttpClient and for container downloads.
71+
/// </summary>
72+
/// <param name="httpClientFactory">The IHttpClientFactory to use for creating HttpClient instances.</param>
73+
/// <param name="conn">The connection information for FMS.</param>
74+
public FileMakerRestClient(
75+
IHttpClientFactory httpClientFactory,
76+
ConnectionInfo conn)
77+
: this(httpClientFactory, new DefaultAuthTokenProvider(conn))
78+
{ }
79+
80+
/// <summary>
81+
/// FM Data Constructor with IHttpClientFactory and an authentication provider.
82+
/// Uses the factory to create the primary HttpClient and for container downloads.
83+
/// </summary>
84+
/// <param name="httpClientFactory">The IHttpClientFactory to use for creating HttpClient instances.</param>
85+
/// <param name="authTokenProvider">Authentication provider.</param>
86+
public FileMakerRestClient(
87+
IHttpClientFactory httpClientFactory,
88+
IAuthTokenProvider authTokenProvider)
89+
: this(httpClientFactory.CreateClient(), authTokenProvider, false)
90+
{
91+
_httpClientFactory = httpClientFactory;
92+
}
93+
#endif
94+
5995
/// <summary>
6096
/// Create a FileMakerRestClient with a new instance of HttpClient.
6197
/// </summary>
@@ -1137,11 +1173,20 @@ protected override async Task<byte[]> GetContainerOnClient(string containerEndPo
11371173
// build the request message
11381174
var requestMessage = new HttpRequestMessage(HttpMethod.Get, containerEndPoint);
11391175

1176+
// determine the client to use for container downloads
1177+
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
1178+
// prefer factory-created clients when available (proper handler pooling)
1179+
var client = _httpClientFactory != null
1180+
? _httpClientFactory.CreateClient()
1181+
: (_useNewClientForContainers
1182+
? new HttpClient(new HttpClientHandler() { CookieContainer = new CookieContainer() })
1183+
: Client);
1184+
#else
11401185
// if we're supposed to use new clients for processing containers or not
1141-
var client = _useNewClientForContainers ? new HttpClient(new HttpClientHandler()
1142-
{
1143-
CookieContainer = new CookieContainer()
1144-
}) : Client;
1186+
var client = _useNewClientForContainers
1187+
? new HttpClient(new HttpClientHandler() { CookieContainer = new CookieContainer() })
1188+
: Client;
1189+
#endif
11451190

11461191
// send the request out
11471192
var data = await client.SendAsync(requestMessage).ConfigureAwait(false);
@@ -1214,18 +1259,12 @@ private async Task<HttpResponseMessage> RetryOnUnauthorizedAsync(Func<Task<HttpR
12141259
return response;
12151260
}
12161261

1217-
/// <summary>
1218-
/// Converts a JToken instance and maps it to the generic type.
1219-
/// </summary>
1220-
/// <typeparam name="T"></typeparam>
1221-
/// <param name="fmId">FileMaker Record Id map function.</param>
1222-
/// <param name="modId">Modification Id map function.</param>
1223-
/// <param name="input">JSON.NET JToken instance from Data Api Response.</param>
1224-
/// <returns></returns>
12251262
/// <summary>
12261263
/// Extracts script result fields (including pre-request and pre-sort) from a response JToken.
12271264
/// Handles dotted property names that cannot be mapped via attributes.
12281265
/// </summary>
1266+
/// <param name="target">The <see cref="ActionResponse"/> to populate with script results.</param>
1267+
/// <param name="responseToken">JSON.NET JToken from the Data API response body.</param>
12291268
private static void PopulateScriptResults(ActionResponse target, JToken responseToken)
12301269
{
12311270
if (target == null || responseToken == null) return;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
2+
using System;
3+
using System.Net;
4+
using System.Net.Http;
5+
using Microsoft.Extensions.DependencyInjection;
6+
7+
namespace FMData.Rest
8+
{
9+
/// <summary>
10+
/// Extension methods for registering FileMakerRestClient with dependency injection.
11+
/// </summary>
12+
public static class ServiceCollectionExtensions
13+
{
14+
/// <summary>
15+
/// Adds a <see cref="FileMakerRestClient"/> to the service collection using <see cref="IHttpClientFactory"/>.
16+
/// Registers the client as both <see cref="IFileMakerApiClient"/> and <see cref="IFileMakerRestClient"/>.
17+
/// </summary>
18+
/// <param name="services">The service collection.</param>
19+
/// <param name="configureConnection">Action to configure the connection info.</param>
20+
/// <returns>The <see cref="IHttpClientBuilder"/> for further HTTP client configuration.</returns>
21+
public static IHttpClientBuilder AddFMDataRest(
22+
this IServiceCollection services,
23+
Action<ConnectionInfo> configureConnection)
24+
{
25+
return AddFMDataRest(services, configureConnection, null);
26+
}
27+
28+
/// <summary>
29+
/// Adds a <see cref="FileMakerRestClient"/> to the service collection using <see cref="IHttpClientFactory"/>.
30+
/// Registers the client as both <see cref="IFileMakerApiClient"/> and <see cref="IFileMakerRestClient"/>.
31+
/// </summary>
32+
/// <param name="services">The service collection.</param>
33+
/// <param name="configureConnection">Action to configure the connection info.</param>
34+
/// <param name="configureHttpClient">Optional action to configure the HttpClient (e.g., set timeouts or default headers).</param>
35+
/// <returns>The <see cref="IHttpClientBuilder"/> for further HTTP client configuration.</returns>
36+
public static IHttpClientBuilder AddFMDataRest(
37+
this IServiceCollection services,
38+
Action<ConnectionInfo> configureConnection,
39+
Action<HttpClient> configureHttpClient)
40+
{
41+
var conn = new ConnectionInfo();
42+
configureConnection(conn);
43+
44+
services.AddSingleton(conn);
45+
services.AddSingleton<IAuthTokenProvider>(new DefaultAuthTokenProvider(conn));
46+
47+
// Register the main FMData named client
48+
var builder = services.AddHttpClient("FMData", client =>
49+
{
50+
configureHttpClient?.Invoke(client);
51+
});
52+
53+
// Register FileMakerRestClient as singleton (preserves auth token state across requests)
54+
services.AddSingleton<FileMakerRestClient>(sp =>
55+
{
56+
var factory = sp.GetRequiredService<IHttpClientFactory>();
57+
var authProvider = sp.GetRequiredService<IAuthTokenProvider>();
58+
return new FileMakerRestClient(factory, authProvider);
59+
});
60+
61+
services.AddSingleton<IFileMakerApiClient>(sp => sp.GetRequiredService<FileMakerRestClient>());
62+
services.AddSingleton<IFileMakerRestClient>(sp => sp.GetRequiredService<FileMakerRestClient>());
63+
64+
return builder;
65+
}
66+
}
67+
}
68+
#endif

src/FMData.Xml/FMData.Xml.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,8 @@
6363
<ItemGroup Condition="$(TargetFramework) == 'net45'">
6464
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
6565
</ItemGroup>
66+
67+
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' AND '$(TargetFramework)' != 'net45'">
68+
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
69+
</ItemGroup>
6670
</Project>

0 commit comments

Comments
 (0)