Formusoft.Integrations.Avigilon.Acm
6.0.5
dotnet add package Formusoft.Integrations.Avigilon.Acm --version 6.0.5
NuGet\Install-Package Formusoft.Integrations.Avigilon.Acm -Version 6.0.5
<PackageReference Include="Formusoft.Integrations.Avigilon.Acm" Version="6.0.5" />
<PackageVersion Include="Formusoft.Integrations.Avigilon.Acm" Version="6.0.5" />
<PackageReference Include="Formusoft.Integrations.Avigilon.Acm" />
paket add Formusoft.Integrations.Avigilon.Acm --version 6.0.5
#r "nuget: Formusoft.Integrations.Avigilon.Acm, 6.0.5"
#:package Formusoft.Integrations.Avigilon.Acm@6.0.5
#addin nuget:?package=Formusoft.Integrations.Avigilon.Acm&version=6.0.5
#tool nuget:?package=Formusoft.Integrations.Avigilon.Acm&version=6.0.5
Formusoft.Integrations.Avigilon.Acm
A reusable .NET 10 client library for Avigilon Access Control Manager 6, based on the uploaded REST_API_ACM_6.44_June2023.pdf, revision 13, dated June 7, 2023. This project is part of Formusoft.Shared.sln. Package versions use the repository's version.json and Nerdbank.GitVersioning settings; the original standalone 0.1.1 version is not the package version for this repository.
Validation status: This is source code, not a certified or appliance-tested SDK. The repository-integration review identified restore/project configuration problems and corrected them. The review environment has no .NET SDK and could not download one, so it did not execute restore, build, tests, or pack. From the repository root, run tools/Test-Packages.ps1 before publishing, then validate the consuming application against an authorized test appliance. Repository review details are in docs/reviews/ACM-PACKAGES-INTEGRATION-REVIEW.md.
Included
The library contains scoped dependency injection, named appliance configurations, typed door/identity/token/role/photo models, cancellation, bounded response reading, Basic authentication over HTTPS, explicit write opt-in, and redacted library logging. It contains no Windows-service host, database, vendor SDK dependency, or background polling loop.
Implemented areas: door inventory and live status; documented door commands and elevator grants; identity lookup, advanced/filter search, create/update, complete role replacement, and user-defined-field updates; token list/create/update/deactivation; JPEG photo upload/read/primary selection; reference inventory; appliance/panel/subpanel/input inventory; raw system and panel-status retrieval; single-alarm acknowledge/clear; global-action execution; and the documented one-time identity CSV collaboration trigger.
The ACM tests use synthetic HTTP responses and do not contact an appliance. This repository does not contain the standalone console sample or its connection-test script. The root Azure pipeline builds the shared solution and runs all tests/**/*.Tests.csproj projects with coverage before packaging.
Publishing destination: The existing pipeline's push step explicitly targets public nuget.org. This review does not change its destination, API-key variable, or publishing behavior. Review or disable that step before committing to main when these packages must remain private. NuGetAuthenticate@1 by itself does not change a later explicit push destination.
Build, test, and pack
Run from the repository root (the folder containing Formusoft.Shared.sln). Requirements: PowerShell 7.2 or later, a .NET 10 SDK, and the targeting packs/package-source access required by the repository's existing target frameworks. The existing Windows CI installs the .NET 6, 8, and 10 SDKs; this patch does not remove older targets from other shared packages. ACM itself remains .NET 10 only.
pwsh -File .\tools\Test-Packages.ps1 -Clean
-Clean removes generated bin/obj folders directly under source and test projects. It does not clear the machine's NuGet cache or change application configuration. Close running build/test processes first. Omit -Clean for an incremental build.
The script restores and builds Formusoft.Shared.sln, runs every discovered *.Tests.csproj under tests/, and verifies that each suite produces TRX results with executed tests. Text logs, TRX files, and coverage files are written beneath artifacts/package-validation/<run-id>/. It stops after a restore/build failure and records all test-suite failures before exiting. It never publishes packages or contacts an ACM appliance.
To create local packages after successful tests:
pwsh -File .\tools\Test-Packages.ps1 -Clean -Pack
Packages are written under that run's packages directory using the repository's existing versioning settings. The script does not override version.json or invent a package version. Consumer applications should reference the version actually published through the authorized NuGet feed; they should not use a source checkout or a project reference into this repository.
The test project has a local reference to src/Integrations/Formusoft.Integrations.Avigilon.Acm/ because it is a development harness inside this packages repository. That is separate from backend consumption through NuGet. Do not add an unapproved public source to work around internal-feed permissions.
Register in the Formusoft backend
In the consuming application's startup code:
using Formusoft.Integrations.Avigilon.Acm;
builder.Services.AddAvigilonAcm(
builder.Configuration.GetSection(AcmOptions.SectionName));
Visible, non-secret defaults can be placed in the host's configuration:
{
"AvigilonAcm": {
"BaseUrl": "https://acm.example.test/",
"Username": "",
"Password": "",
"AcceptAnyServerCertificate": false,
"RequestTimeout": "00:00:30",
"MaximumResponseBytes": 8388608,
"MaximumRequestBodyBytes": 4194304,
"MaximumRequestUriLength": 16384,
"DateFormat": "MM/dd/yyyy",
"EnableWrites": false,
"CaptureErrorResponseBody": false
}
}
Set the real URL and supply the username/password through the backend's protected configuration or secret provider. Environment configuration keys are AvigilonAcm__BaseUrl, AvigilonAcm__Username, and AvigilonAcm__Password. These are configuration key names, not credentials to commit. An ECS startup-secret mapping can supply those variables using the application's existing deployment process.
BaseUrl must be the HTTPS appliance origin, optionally including its port. A subpath, query, fragment, or embedded username/password is rejected. Certificate validation is enabled by default. Configure a correctly named certificate and its approved trust chain in the machine/container trust store. The library does not alter the host trust store.
Optional automatic certificate acceptance
Merge this setting into the existing connection section to explicitly bypass server-certificate validation:
{
"AvigilonAcm": {
"AcceptAnyServerCertificate": true
}
}
The equivalent environment variable is AvigilonAcm__AcceptAnyServerCertificate=true, or set options.AcceptAnyServerCertificate = true in the action-based registration overload. For named appliances, put the flag in that appliance's own configuration section, for example AcmSites:Main:AcceptAnyServerCertificate. The default is false when the option is omitted.
This is a validation bypass, not certificate installation, pinning, or trust-on-first-use. It accepts any server certificate, including untrusted/self-signed, expired, and hostname-mismatched certificates. HTTPS encryption and ACM username/password authentication are still required, but the server's identity is not verified. Credentials and data may be intercepted. Use only for an explicitly approved test or isolated legacy appliance connection; a private network alone does not authenticate a server. For normal production use, leave the flag false and establish approved certificate trust.
The override applies only to the connection configured with this flag. It does not change process-wide TLS callbacks, other application HTTP clients, or the operating-system trust store. Cookies and redirects remain disabled. A warning without credentials, URLs, or certificate contents is logged on the first bypass request from each ACM client instance.
There are separate, fixed HTTP handler pools for strict and bypass policies. The original AcmServiceCollectionExtensions.HttpClientName always selects the strict policy; AcceptAnyCertificateHttpClientName is the separate bypass transport. Hosts that intentionally customize the transport (for example, a proxy) must configure each transport they use; a custom primary handler is responsible for its own TLS behavior.
After a reload-capable configuration provider reloads, a new DI scope observes the new flag. Existing scopes/clients retain their immutable settings, including the old flag. Strict clients never borrow an existing bypass-policy HTTP connection. Drain/dispose old clients/scopes when immediately removing a previously enabled bypass. Startup environment variables supplied by ECS do not become reloadable merely because the library uses options snapshots; changing such values requires the normal task restart/redeployment.
Read doors and status
Inject IAcmClient for the full surface, or IAcmDoorService, IAcmIdentityService, or the other service interfaces for a narrower dependency.
using Formusoft.Integrations.Avigilon.Acm;
public sealed class DoorInventoryService(IAcmClient acm)
{
public Task<IReadOnlyList<AcmDoor>> GetDoorsAsync(CancellationToken ct)
=> acm.Doors.ListAsync(ct);
public Task<IReadOnlyList<AcmDoorStatus>> GetStatusAsync(
IReadOnlyCollection<string> fullDoorDns,
CancellationToken ct)
=> acm.Doors.GetStatusAsync(fullDoorDns, ct);
}
For each requested DN, status results preserve the returned mode and ordered raw statuses. Missing or unrecognized conditions map to AcmCondition.Unknown, not Normal. WasReported identifies DNs missing from both response arrays. The door-status client implements the API 1.04+ response shape, not the earlier numeric-element XML format. This request has an XML body on GET; the network path must preserve that body. (Manual pp. 73–77.)
To use enhanced inventory, deliberately choose the enhanced method:
var doors = await acm.Doors.ListEnhancedAsync(new AcmInventoryQuery
{
Installed = true,
Attributes = ["dn", "plasecName", "plasecInstalled"]
}, ct);
Attribute selection on /rest endpoints requires API 1.10+, and installed/inuse filtering requires API 1.11+. The client does not probe invented version endpoints or silently fall back to an unfiltered query. (Manual pp. 34–37.)
Identity operations
Identity searches require criteria. A raw LDAP filter method exists for experienced callers; prefer AcmLdapFilter.Equal or StartsWith when constructing filters from values. The advanced search method uses the document's zero-based adv_search_cnt and matching field/value/pattern indices. Numeric comparison patterns are restricted to their documented fields; the ambiguous date-comparison examples are not turned into an invented date-search API.
var matches = await acm.Identities.FindByExternalSystemIdAsync("formusoft:employee:123", ct);
var search = new AcmIdentitySearch
{
Criteria = [new AcmSearchCriterion("plasecLname", AcmSearchPattern.BeginsWith, "Example")],
IncludeUserDefinedFields = true
};
var identities = await acm.Identities.SearchAsync(search, ct);
Writes require the host to set EnableWrites to true deliberately. ACM permissions and your application's authorization must still be enforced. Example request construction:
var receipt = await acm.Identities.CreateAsync(new AcmIdentityCreateRequest
{
LastName = "Example",
FirstName = "Integration",
Status = 1,
ExternalSystemId = "formusoft:employee:123"
}, ct);
Create returns an AcmResponse receipt, including an unfollowed Location header when supplied. The guide does not guarantee a created-record response body, so the client does not invent a CN. Inspect the receipt, or re-query your external-system ID and require exactly one match before proceeding. The library does not assume ACM enforces uniqueness for that external ID and does not implement an automatic upsert or retry.
Updates omit null fields; an empty string sends an explicit empty value. Whether a field may be cleared remains subject to ACM validation. For example, new AcmIdentityUpdateRequest { Department = "Operations" } updates only that field. Status 1 is documented as active; other identity statuses are not assigned hard-coded meanings.
Role updates replace the entire assignment list. ReplaceRolesAsync sends <sel_members type="array"> as the XML root, not an identity object, and does not hide a read/merge/write operation. Passing an empty list requires allowRemoveAll: true. Application code must coordinate concurrent role changes and re-read after a write. (Manual pp. 123–124.)
UpdateUserDefinedFieldsAsync takes literal ACM field names without the UDF_ prefix. It URL-encodes UDF_<name> and each value, including spaces; it sends an empty request body. Empty strings clear values. Date UDF values must already match the integration account's format. Response UDF names are preserved exactly as the XML returns them; display-name-to-XML-name mapping is not guessed. (Manual pp. 125–128.)
Tokens and dates
Create and update both require an AcmTokenDates object with explicit issue, activation, and deactivation dates. These are URL parameters, not XML fields. Requiring an explicit expiration date is a library safety choice that avoids accepting the guide's documented fallback expiration date when an update omits it. (Manual pp. 168–172.)
var request = new AcmTokenCreateRequest
{
InternalNumber = "1234567890",
Status = 1,
Dates = new AcmTokenDates
{
IssueDate = new DateOnly(2026, 9, 16),
ActivateDate = new DateOnly(2026, 9, 16),
DeactivateDate = new DateOnly(2027, 9, 16)
},
Download = true
};
var receipt = await acm.Tokens.CreateAsync(identityCn, request, ct);
The dates above illustrate request construction only. Choose dates under your own access policy and match DateFormat to the ACM integration identity's locale. Do not blindly truncate a returned UTC timestamp into an account-local date. The library returns token date fields as raw strings and does not guess their timezone conversion.
Internal and embossed numbers are strings so large values are not truncated to a .NET integer. Internal numbers are validated as ASCII digits. PINs, when supplied, are numeric and at most eight characters. ACM, not this client, enforces token-number/PIN uniqueness, parent-identity activity, activation conditions, and active-token limits. Only the documented PIV=2 and PIV-I=3 type codes are enumerated; conventional tokens omit that field.
DeactivateAsync defaults to the documented token status 2, permits another site-configured inactive code, rejects active status 1, and requires the caller to supply the existing dates. ListAllAsync is the explicit opt-in for the -1 identity sentinel; per-identity methods reject it. No destructive delete methods are exposed.
Multiple appliances and reloadable settings
builder.Services.AddAvigilonAcm(
builder.Configuration.GetSection("AcmSites:Main"), "Main");
builder.Services.AddAvigilonAcm(
builder.Configuration.GetSection("AcmSites:Annex"), "Annex");
// In a scoped service:
IAcmClient main = factory.CreateClient("Main");
IAcmClient annex = factory.CreateClient("Annex");
IAcmClientFactory is scoped, using IOptionsSnapshot<AcmOptions>. A new application/worker scope observes configuration reloads; an existing scope keeps its snapshot. A worker must create a fresh DI scope for each unit of work instead of injecting the scoped factory into its singleton. Named-only registrations must be consumed through the factory; direct IAcmClient injection resolves the Default configuration.
Names must be registered deliberately. Do not select an appliance solely from an untrusted request value: first authorize the user/tenant against that connection. Authentication is added to each request, not stored on a shared handler or mutable global default header. Two fixed pooled HTTP transports separate strict and bypass certificate policies across configurations, with cookies disabled. A named appliance selects its transport from its own immutable settings snapshot.
Read-only appliance validation
No console sample or Test-AcmConnection.ps1 script is present in this repository. Do not use instructions for the old standalone solution here. After the local build and tests pass, validate through an approved consuming host that registers this package, supplies the dedicated integration credentials through protected configuration, and keeps EnableWrites=false while testing door inventory/status reads.
The tools/Test-Packages.ps1 helper performs build/unit-test validation only. It cannot establish appliance reachability, API licensing, account permissions, or real TLS-handshake behavior.
Error handling and security boundaries
try
{
var doors = await acm.Doors.ListAsync(ct);
}
catch (AcmException ex)
{
logger.LogWarning("ACM operation {Operation} failed: {FailureKind}; HTTP={Status}",
ex.Operation, ex.FailureKind, ex.StatusCode);
}
The client rejects non-success HTTP statuses and HTML login pages. Ordinary response paths check common XML/JSON error envelopes, including errors returned with HTTP 200. This check is deliberately called CommonErrorEnvelopeChecked, not “business operation succeeded.” Unknown plain-text command receipts remain unclassified. Always check application results and, for physical commands, independently verify device state. A successful HTTP response does not prove a door changed state.
There is no library retry policy for reads or writes and no redirect following. A timeout, cancellation, or transport failure after a write can leave its outcome unknown. AcmException.WriteMayHaveBeenApplied highlights dispatched writes; cancellation also does not roll back a request. Never blindly retry a grant, creation, or other side-effecting call.
Error messages and the library's logging omit credentials, request URLs, and response payloads. Default HTTP-client URL logging is removed for this transport. CaptureErrorResponseBody is false by default; its opt-in diagnostic body is capped and is not in ToString. Models do not dump all their data in ToString; raw XML/response bodies require explicit access. They can still contain personal information, PINs, password hashes, or card numbers. Do not log them or expose them directly through a public API. Use carefully selected application DTOs.
Host-level OpenTelemetry, .NET network diagnostics, reverse proxies, and other middleware are outside this library's logging controls. Configure them to avoid collecting ACM query parameters, DNs, credentials, and response bodies. UDF values and token dates are necessarily on the URL because the documented protocol requires it.
Intentional limits
GetSystemStatusRawAsync returns the source payload because the guide lists endpoints but does not supply a complete response schema. GetPanelStatusRawAsync preserves the panel/subpanel response: the printed example contains numeric element names such as <16>, which are not standard XML names. The library does not silently repair them or invent meanings for numeric status codes. Raw responses check HTTP status and reject obvious HTML, but their business/error payload is not interpreted; inspect Body using a site-validated parser. ParseXml() only accepts well-formed XML. (Manual pp. 53, 148–151, 164.)
This is not an implementation of every operation in the 177-page guide. It excludes destructive deletes; bulk alarm processing; door/panel/appliance configuration writes; access-group/group/holiday CRUD; intrusion-panel management; undocumented pagination; and real-time transaction ingestion. /points.xml describes event configuration, not card-swipe history. Live event delivery requires separately configured ACM collaborations and their documentation. The one-time CSV collaboration trigger included here is not that event receiver. (Manual pp. 46, 62–63, 80, 84.)
The guide also notes REST API licensing, use of a dedicated integration identity, and that ACM MFA prevents REST authentication. Have the ACM administrator approve the integration account and its least-privilege permissions; do not disable organization-wide MFA as a workaround. Appliance/version/permission validation is required before production use. (Manual pp. 13–15.)
For this repository's build integration, see docs/reviews/ACM-PACKAGES-INTEGRATION-REVIEW.md from the repository root. The original standalone docs/API-COVERAGE.md, docs/DOCUMENT-NOTES.md, and docs/VALIDATION.md files were not included in the uploaded packages repository; the scope limits above remain applicable.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.0
- Microsoft.Extensions.Http (>= 10.0.12)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.12)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 6.0.5 | 114 | 9/21/2026 |