SuccessMortgagePartners.Encompass.Client
0.2.0
See the version list below for details.
dotnet add package SuccessMortgagePartners.Encompass.Client --version 0.2.0
NuGet\Install-Package SuccessMortgagePartners.Encompass.Client -Version 0.2.0
<PackageReference Include="SuccessMortgagePartners.Encompass.Client" Version="0.2.0" />
<PackageVersion Include="SuccessMortgagePartners.Encompass.Client" Version="0.2.0" />
<PackageReference Include="SuccessMortgagePartners.Encompass.Client" />
paket add SuccessMortgagePartners.Encompass.Client --version 0.2.0
#r "nuget: SuccessMortgagePartners.Encompass.Client, 0.2.0"
#:package SuccessMortgagePartners.Encompass.Client@0.2.0
#addin nuget:?package=SuccessMortgagePartners.Encompass.Client&version=0.2.0
#tool nuget:?package=SuccessMortgagePartners.Encompass.Client&version=0.2.0
SuccessMortgagePartners.Encompass.Client
A .NET client for the ICE Mortgage Technology Encompass Developer Connect API.
- OAuth endpoints: password grant, token exchange (act as another user), introspection, revocation
- Cached API token that follows Encompass's lifetime rules (kept alive by use at least every 15 min, 24 h max)
- Per-user calls:
ForUser(encompassUserId)acts as another Encompass user through token exchange, with one cached token per user - Users: list internal users and look them up by login id, email or employee id
IHttpClientFactoryintegration and options binding (AddEncompassClient)- API areas are added one at a time as each is verified against the live API (auth and users today)
- Targets
net10.0andnetstandard2.0
Status: early. The surface area grows as we wrap more endpoints; see Extending.
Install
dotnet add package SuccessMortgagePartners.Encompass.Client
Configure
appsettings.json (or environment variables Encompass__ClientId, etc.):
{
"Encompass": {
"ClientId": "...",
"ClientSecret": "...",
"InstanceId": "BE11111111",
"Username": "apiuser",
"Password": "..."
}
}
BaseAddress defaults to https://api.elliemae.com/; if you point it at a path (for example a proxy), end it with / (and note that a request path starting with / resolves to the host root, bypassing the prefix). Never commit real credentials; use user-secrets, Key Vault, or app settings.
Optional throttling settings: MaxConcurrentRequests (default: follow 80% of the limit Encompass reports, starting at 24) and ConcurrencyLimitRetries (default 3) control how the client stays under Encompass's concurrency limit; see "Concurrency" below.
Use
using SuccessMortgagePartners.Encompass.Client;
using SuccessMortgagePartners.Encompass.Client.DependencyInjection;
builder.Services.AddEncompassClient(builder.Configuration); // binds the "Encompass" section
// later, via DI
public sealed class WhoAmI(IEncompassClient encompass)
{
public async Task<string?> CurrentUserAsync(CancellationToken ct)
{
var token = await encompass.Auth.RequestPasswordTokenAsync(ct);
try
{
var info = await encompass.Auth.IntrospectAsync(token.AccessToken, ct);
return info.UserName;
}
finally
{
// Not `ct`: a cancelled caller must still revoke the token it created.
await encompass.Auth.RevokeAsync(token.AccessToken, CancellationToken.None);
}
}
}
This example requests a token explicitly because it exercises the OAuth endpoints themselves. API resource groups (added one area at a time) don't do this: they send requests through the registered IAccessTokenProvider, which caches one password-grant token and keeps it alive while it is in use.
encompass.Auth.ExchangeTokenAsync(actorToken, subjectUserId) returns a token that acts as another Encompass user, with that user's permissions.
Acting as a user
A service that serves several people (for example behind Entra sign-in) can make each call as the person it is for, so Encompass applies that user's persona and loan access and its audit trail shows them:
var matches = await encompass.Users.FindByEmailAsync("jane.doe@example.com", ct);
if (matches.Count != 1 || matches[0].AccountEnabled != true)
{
throw new UnauthorizedAccessException("No single enabled Encompass user for this person.");
}
var user = matches[0];
IEncompassClient asUser = encompass.ForUser(user.Id); // cheap; create one per request
// asUser.<Area>.… calls now run as user.Id. asUser.ActingUserId == user.Id.
The id passed to ForUser decides whose access every call gets, so take it only from a mapping you trust (for example a verified employee id, then email, then a reviewed override list), never from request or model input. Run lookups on the API-user client: most users can't list users.
ForUser exchanges the configured API user's token for the subject (IUserTokenProvider, registered as a singleton) and caches one token per user with the same sliding expiry as the API token. A 401 on an API call drops that user's token and retries once with a new exchange; a 401 (or 400 invalid_grant/invalid_token) on the exchange itself retries with the API user's current token if another call already renewed it; otherwise, only if introspection shows the API user's token is no longer active, it drops that token (the next call gets a new one) and, at most once a minute, renews it at once and tries again. If no token can be had, the call throws and nothing is sent: a ForUser client never falls back to the API user. Concurrent calls for one user share one exchange. Encompass only allows the exchange when the API user is at or above the subject in the organization hierarchy (Super Administrators can act as anyone); other refusals surface as EncompassApiException.
Users
encompass.Users wraps GET /encompass/v3/users. ListAsync returns one page (UserListRequest maps to orgId, isRecursive, entities, filter, start, limit; at most 100 per page) and ListAllAsync reads pages until an empty one. FindByIdAsync, FindByEmailAsync and FindByEmployeeIdAsync search the root organization and all children and request the Summary,AccountInformation entities. Encompass can't filter on employee id, so FindByEmployeeIdAsync reads every user: cache its result. Encompass only returns users to administrators or personas with the Settings "Organizations/User" right.
IEncompassClient and the resource interfaces are meant to be consumed, not implemented, by applications: a new API area adds a member, which is a minor version bump. Mock the resource interfaces your code uses; a hand-written fake that implements IEncompassClient must expect new members on each update.
Failures throw EncompassApiException with StatusCode, ErrorCode (Encompass errorCode or OAuth error), CorrelationId (the X-Correlation-ID header, which ICE support asks for), and the raw ResponseBody.
Concurrency
Encompass allows a fixed number of concurrent API requests per environment (30 by default) and answers 429 Too Many Requests beyond it; the counter resets every second. Every API response reports the allowance in X-Concurrency-Limit-Limit and X-Concurrency-Limit-Remaining (OAuth responses don't). The client:
- holds API requests in a gate so no more than
MaxConcurrentRequestsare in flight. By default the cap follows 80% of the reported limit, the share ICE recommends lenders stay within, starting at 24 until the first response; - retries a
429after 1 s, 2 s, 4 s (orRetry-After), up toConcurrencyLimitRetriestimes, then returns it to the caller; - retries once with a fresh token after a
401(every token that gets a401is dropped from the cache, so a second401is returned to you without leaving a known-bad token behind); - exposes the last reported allowance as
IEncompassClient.LastConcurrencyLimit.
Each 429 back-off is capped at 30 s (also when Retry-After asks for more) and adds up to 500 ms of random jitter. Redirects are never followed, so the bearer and Basic headers only ever go to BaseAddress. The cap is per process: if you run several instances against one Encompass environment (for example a scaled-out Azure Functions app), set MaxConcurrentRequests to roughly 80% of the limit divided by the instance count.
Extending
Add a resource group per API area (Loans, Documents, Pipeline, …), verified against the API reference and the integration tests:
- Interface in
src/.../<Area>/I<Area>Resource.cs, implementation beside it (internal sealed). - Expose it on
IEncompassClientand construct it inEncompassClient. - Send requests through
EncompassClient.SendAsync(internal) andEnsureEncompassSuccessAsync, so the bearer token and error handling stay in one place. - Unit tests in
tests/…Client.TestsusingFakeHttpMessageHandler(assert the exact URL, method, and body), and real calls intests/…Client.IntegrationTests(see its README).
Return JsonElement until a payload is stable enough to deserve a model; typed models go in <Area>/Models/.
Build and test
dotnet build
dotnet test
CI and release
CI runs on Azure Pipelines from azure-pipelines.yml; the code stays on GitHub.
developis the default branch. Pull requests targetdevelop.- Every push and pull request to
developormainbuilds, tests and packs. - Every merge (push) to
mainalso pushes the package to NuGet.org and creates thevX.Y.Ztag and GitHub release. The version is<Version>insrc/SuccessMortgagePartners.Encompass.Client/SuccessMortgagePartners.Encompass.Client.csproj; a version with a suffix (1.2.0-preview.1) is published as a prerelease. - To release: bump
<Version>ondevelop, then mergedevelopintomain. If that version is already on NuGet.org from another commit, the run warns and publishes nothing (the pull request intomainshows the same warning); if NuGet.org is still indexing it, the push fails instead, which is also safe. If a release run fails after the NuGet push, wait until the version shows on nuget.org, then use Rerun failed jobs on that run: it sees the package came from that commit and creates or updates the GitHub release. (A new run only does this whilemainstill points at that commit; otherwise create the GitHub release by hand.) Don't push release tags by hand.
One-time setup in Azure DevOps:
- In the Development project, Pipelines → New pipeline → GitHub (YAML), pick this repo and the existing
azure-pipelines.yml. Use the existinggithub.com_SuccessMortgagePartnersconnection. - Library → Variable group
encompass-client-releasewith secret variableNuGetApiKey(a NuGet.org key with Push new packages and package versions for the globSuccessMortgagePartners.*; 0.1.0 creates the package). Allow the pipeline to use it, and add a Branch control check that allows onlyrefs/heads/main, so no other branch can read the key. - Service connection (GitHub) named
github-releases-encompass-client, for creating the tag and GitHub release. GitHubRelease@1 only accepts OAuth ("Grant authorization") or personal access token connections, so the existing GitHub App connectiongithub.com_SuccessMortgagePartnerscan't be used. With a token, use a fine-grained one limited to this repo with Contents: read and write. Either way, add a Branch control check allowing onlyrefs/heads/main. Tokens expire and OAuth grants follow their user's access: when it stops working, the release step fails after the NuGet push; renew the token, update the connection, then use Rerun failed jobs. - Environment
encompass-client-nuget, with a Branch control check forrefs/heads/main(the Release stage'slockBehavior: sequentialalready makes two quick merges publish one at a time). Add an approval check too if you want a person to confirm each publish. - Set
releaseEnabled: 'true'inazure-pipelines.yml. Until then, runs onmainbuild and pack but don't publish.
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
- System.Text.Json (>= 10.0.0)
-
net10.0
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.