ControlId.Net.AspNetCore
0.4.1-alpha.1
See the version list below for details.
dotnet add package ControlId.Net.AspNetCore --version 0.4.1-alpha.1
NuGet\Install-Package ControlId.Net.AspNetCore -Version 0.4.1-alpha.1
<PackageReference Include="ControlId.Net.AspNetCore" Version="0.4.1-alpha.1" />
<PackageVersion Include="ControlId.Net.AspNetCore" Version="0.4.1-alpha.1" />
<PackageReference Include="ControlId.Net.AspNetCore" />
paket add ControlId.Net.AspNetCore --version 0.4.1-alpha.1
#r "nuget: ControlId.Net.AspNetCore, 0.4.1-alpha.1"
#:package ControlId.Net.AspNetCore@0.4.1-alpha.1
#addin nuget:?package=ControlId.Net.AspNetCore&version=0.4.1-alpha.1&prerelease
#tool nuget:?package=ControlId.Net.AspNetCore&version=0.4.1-alpha.1&prerelease
ControlId.Net
Cliente .NET não oficial para equipamentos Control iD:
- Linha de acesso: iDAccess, iDFlex, iDFace, iDBox, iDUHF (API
*.fcgisobre HTTP). - Relógios de ponto REP iDClass: API HTTPS, incluindo AFD no layout da Portaria 671.
Alvos: net8.0 e netstandard2.0, então também roda em projetos .NET Framework 4.6.1+. A suíte de testes roda no .NET 8, no .NET 10 e no .NET Framework 4.8.
Projeto independente, sem vínculo com a Control iD. "Control iD", "iDAccess", "iDFlex", "iDFace" e "iDClass" são marcas da Control iD.
Instalação
dotnet add package ControlId.Net --prerelease
Pacotes complementares:
| Pacote | Para quê |
|---|---|
ControlId.Net.DependencyInjection |
AddControlIdAccess / AddControlIdRep no IServiceCollection, com vários equipamentos e appsettings.json |
ControlId.Net.AspNetCore |
Rotas de Push e Monitor no ASP.NET Core (net8.0+); inclui o pacote de DI |
Linha de acesso
O pacote cobre toda a documentação pública da API de acesso: os 85 endpoints, os 37 objetos, os eventos do Monitor, do Push e do modo online e as 34 seções de configuração têm métodos, modelos ou rotas tipados. A matriz completa, com as divergências da documentação e como foram tratadas, está em docs/COBERTURA.md.
using ControlId.Access;
using var device = new AccessClient("http://192.168.0.129", "admin", "admin");
// O login é feito sozinho na primeira chamada e refeito se a sessão expirar.
var users = await device.GetUsersAsync();
var id = await device.CreateUserAsync(new AccessUser { Name = "Maria", Registration = "1020" });
await device.OpenDoorAsync(door: 1);
await device.ReleaseTurnstileAsync(TurnstileDirection.Clockwise);
var logs = await device.GetAccessLogsAsync(afterId: 0, limit: 100);
Qualquer objeto da API (groups, access_rules, cards, templates...) pode ser lido e escrito pelos métodos genéricos:
var cards = await device.LoadObjectsAsync<AccessCard>(new LoadObjectsRequest("cards")
{
Where = Where.Equal("cards", "user_id", id),
});
await device.ModifyObjectsAsync("users", new { name = "Maria Silva" }, Where.Equal("users", "id", id));
await device.DestroyObjectsAsync("cards", Where.In("cards", "id", new[] { 1L, 2L }));
Cartões, digitais e fotos
await device.CreateCardAsync(new AccessCard { UserId = id, Value = 123456789 });
var digitais = await device.GetTemplatesAsync(userId: id);
await device.SetUserImageAsync(id, File.ReadAllBytes("maria.jpg")); // JPEG < 1 MB
byte[] foto = await device.GetUserImageAsync(id);
Áreas, feriados, eventos, alarmes e SecBox
var garagem = await device.CreateAreaAsync(new AccessArea { Name = "Garagem" });
await device.LinkAsync(AccessLink.AreaAccessRule(garagem, regra));
await device.CreateHolidayAsync(AccessHoliday.FullDay("Natal", new DateTime(2026, 12, 25), TimeSpan.FromHours(-3), repeats: true));
var aberturas = await device.GetAccessEventsAsync(afterId: ultimoEvento); // portas, SecBox e catraca
var alarmes = await device.GetAlarmLogsAsync(afterId: ultimoAlarme);
var secBox = await device.GetSecBoxAsync(); // iDFlex, iDAccess Pro/Nano
secBox!.RelayTimeoutMilliseconds = 5000;
await device.UpdateSecBoxAsync(secBox);
Relatórios e exportações
string csv = await device.GenerateReportAsync(new ReportRequest("users")
{
Delimiter = ";",
Header = "Nome;Matrícula",
Columns = { ReportColumn.ObjectField("users", "name"), ReportColumn.ObjectField("users", "registration") },
});
AfdFile afd = await device.ExportAfdFileAsync(initialNsr: 1); // AFD do equipamento de acesso (iDFlex Attendance)
string auditoria = await device.ExportAuditLogsAsync(AuditLogCategories.Config | AuditLogCategories.Api);
Os textos devolvidos são decodificados como o AFD do REP: usa o charset informado; sem ele, UTF-8 se for válido e, senão, ISO-8859-1.
Cadastro remoto (cartão, face, digital, PIN, senha)
O equipamento pede a credencial à pessoa e devolve o resultado:
// Síncrono: a chamada espera a pessoa concluir.
var resultado = await device.RemoteEnrollAsync(new RemoteEnrollRequest(RemoteEnrollType.Face)
{
UserId = id,
Save = true, // grava no usuário; false só captura e devolve
Auto = true,
Countdown = TimeSpan.FromSeconds(3),
});
if (!resultado.Success)
{
Console.WriteLine(resultado.Error); // ex.: FACE_EXISTS (resultado.MatchUserId diz de quem é a face)
}
- A espera síncrona é limitada pelo
Timeoutdo cliente (30 s por padrão). Se ele estourar ou oCancellationTokenfor cancelado, o cadastro é cancelado no equipamento (cancel_remote_enroll), para ele não ficar preso na tela de cadastro. - Para esperas longas, use
StartRemoteEnrollAsync(...): a chamada volta na hora e o resultado chega pelo Monitor comoRemoteEnrollNotification(comEnrollTypee o mesmoRemoteEnrollResult). - Com o equipamento em modo online, o resultado assíncrono não vai pelo Monitor: chega em
card_create,face_create,fingerprint_create,template_create,pin_createoupassword_create, que oMapControlIdOnlineentrega emIOnlineAccessHandler.OnRemoteEnrollAsync. Nesse caso a foto da face vem emResult.Face. - Via Push:
PushCommand.RemoteEnroll(request)eresult.ResponseAs<RemoteEnrollResult>(). - Falhas do cadastro (timeout da pessoa, face repetida) vêm em
Error, sem exceção; useEnsureSuccess()se preferir exceção.
Grupos, horários e regras de acesso
var grupo = await device.CreateGroupAsync(new AccessGroup { Name = "Administrativo" });
var horario = await device.CreateTimeZoneAsync(new AccessTimeZone { Name = "Comercial" });
await device.CreateTimeSpanAsync(AccessTimeSpan.Weekdays(horario, TimeSpan.FromHours(8), TimeSpan.FromHours(18)));
var regra = await device.CreateAccessRuleAsync(new AccessRule { Name = "Comercial", Type = AccessRuleType.Allow, Priority = 0 });
await device.LinkAsync(AccessLink.UserGroup(id, grupo));
await device.LinkAsync(AccessLink.GroupAccessRule(grupo, regra));
await device.LinkAsync(AccessLink.AccessRuleTimeZone(regra, horario));
await device.LinkAsync(AccessLink.PortalAccessRule(portalId: 1, regra));
Comandos do equipamento
await device.ShowMessageAsync("Bem-vindo!", TimeSpan.FromSeconds(5)); // sem duração: fica até ser trocada
await device.BuzzAsync(TimeSpan.FromMilliseconds(500)); // até 3 s por chamada
await device.SetLogoAsync(File.ReadAllBytes("logo.png"), slot: 1);
await device.ShowLogoAsync(1);
var sensor = await device.GetGpioStateAsync(5); // iDAccess: sensor de porta 1
var hash = await device.HashPasswordAsync("1234"); // para AccessUser.Password/Salt
await device.ChangeLoginAsync("operador", "nova-senha"); // o cliente passa a usar as novas credenciais
await device.SetNetworkAsync(new NetworkSettings("192.168.0.33", "255.255.255.0", "192.168.0.1"));
await device.RebootAsync();
| Método | Endpoint |
|---|---|
ShowMessageAsync / ClearMessageAsync |
message_to_screen |
BuzzAsync |
buzzer_buzz |
ChangeLoginAsync |
change_login |
HashPasswordAsync |
user_hash_password |
RebootAsync / RebootToUpdateModeAsync |
reboot / reboot_recovery |
ResetToFactoryDefaultAsync (apaga tudo) |
reset_to_factory_default |
DeleteAllAdministratorsAsync |
delete_admins |
SetNetworkAsync |
set_system_network |
GetLogoAsync / SetLogoAsync / DeleteLogoAsync / ShowLogoAsync |
logo, logo_change, logo_destroy, general.show_logo |
GetGpioStateAsync |
gpio_state |
RefreshLedAsync |
led_rgb_refresh |
SetUsbPortEnabledAsync |
general.usb_port_enabled |
CreateOrModifyObjectsAsync |
create_or_modify_objects |
Via Push: PushCommand.ShowMessage, PushCommand.Buzz e PushCommand.Reboot.
Configurações
Cada seção documentada tem uma classe tipada em ControlId.Access.Configuration:
using ControlId.Access.Configuration;
var geral = await device.GetConfigurationSectionAsync<GeneralSettings>();
Console.WriteLine(geral.Language);
// Envia só o que foi preenchido; várias seções numa chamada.
await device.SetConfigurationSectionsAsync(new ConfigurationSection[]
{
new GeneralSettings { BeepEnabled = true, CatraTimeout = 3000 },
new PjsipSettings { Enabled = true, ServerIp = "pbx.local", ServerPort = 5060 },
});
O acesso genérico, por nome de seção e campo, continua disponível:
var monitor = await device.GetConfigurationAsync("monitor", new[] { "hostname", "port" });
await device.SetConfigurationAsync(new { general = new { beep_enabled = "1" } }); // valores sempre em texto
Qualquer objeto, tipado
Todos os objetos da documentação têm modelo e funcionam com o CRUD genérico:
var alarmes = await device.GetObjectsAsync<TimedAlarm>();
await device.CreateObjectAsync(TimedAlarm.Weekdays("Almoço", TimeSpan.FromHours(12)));
await device.UpdateObjectAsync(new AlarmZone { Zone = 1, Enabled = true });
await device.DeleteObjectAsync<ScheduledUnlock>(3);
Endpoints sem método próprio
A API tem muitos comandos que ainda não têm método tipado (cadastro remoto, mensagem na tela, rede, logotipo,
reinício...). Todos podem ser chamados pelos métodos genéricos, que valem para AccessClient e RepClient
(interface IControlIdClient) e mantêm a sessão e o re-login automáticos:
await device.SendRawAsync("message_to_screen", new { message = "Bem-vindo!", timeout = 5000 });
var resposta = await device.SendAsync<JsonElement>("endpoint", corpo, queryString: "user_id=7");
await device.SendBytesAsync("endpoint", bytes, "application/octet-stream", "id=1"); // corpo binário
byte[] arquivo = await device.DownloadAsync("endpoint", method: HttpMethod.Get); // resposta binária
O endpoint pode vir com ou sem .fcgi. Consulte o corpo de cada comando na
documentação da Control iD. No REP, o mode=671 não é
acrescentado automaticamente nessas chamadas: passe-o em queryString quando o comando exigir.
Monitor e Push (tempo real)
Nos dois modos é o equipamento que chama o seu servidor. A biblioteca não sobe servidor HTTP: ela interpreta as notificações (Monitor) e mantém a fila de comandos (Push), e você liga isso nas rotas do framework que já usa.
- Monitor: o equipamento faz
POSTem{path}/{endpoint}a cada evento (log de acesso, porta, catraca, modo de operação...). - Push: o equipamento consulta
GET /pushperiodicamente, executa o comando recebido e devolve o resultado emPOST /result. Serve para equipamentos atrás de NAT, sem IP acessível.
// Uma vez, pelo cliente HTTP normal:
await device.ConfigureMonitorAsync(new MonitorConfiguration { Hostname = "192.168.0.20", Port = 5000, Path = "api/notifications" });
await device.ConfigurePushAsync(new PushConfiguration { RemoteAddress = "192.168.0.20:5000", RequestPeriod = TimeSpan.FromSeconds(5) });
Com ASP.NET Core, o pacote ControlId.Net.AspNetCore monta as rotas:
dotnet add package ControlId.Net.AspNetCore --prerelease
using ControlId.Access.Monitoring;
using ControlId.Access.Push;
using ControlId.AspNetCore;
builder.Services.AddControlIdPush(); // PushServer singleton
builder.Services.AddControlIdMonitor<MeuHandler>(); // ou AddControlIdMonitor((notificacao, ct) => ...)
app.MapControlIdPush(); // GET /push e POST /result
app.MapControlIdMonitor(); // POST /api/notifications/{endpoint}
public sealed class MeuHandler : IMonitorNotificationHandler // scoped: pode receber DbContext etc.
{
public Task HandleAsync(MonitorNotification notificacao, CancellationToken ct)
{
if (notificacao is DaoNotification dao)
{
foreach (var log in dao.GetInsertedAccessLogs())
{
Console.WriteLine($"Acesso: usuário {log.UserId}, evento {log.Event}");
}
}
return Task.CompletedTask;
}
}
Os dois Map... devolvem o RouteGroupBuilder, então dá para encadear RequireHost, filtros etc.
O equipamento espera a resposta HTTP do Monitor: mantenha o handler curto ou jogue o trabalho numa fila.
Em qualquer lugar da aplicação, comande o equipamento pelo PushServer injetado. Cada chamada espera o
equipamento buscar o comando e responder:
using var prazo = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await push.Device(deviceId).OpenDoorAsync(door: 1, prazo.Token);
var usuarios = await push.Device(deviceId).LoadObjectsAsync<AccessUser>(new LoadObjectsRequest("users"), prazo.Token);
// Vários comandos numa única consulta do equipamento (objeto "transactions"):
var resultados = await push.Device(deviceId).SendBatchAsync(new[]
{
PushCommand.SetConfiguration(new { general = new { beep_enabled = "0" } }),
PushCommand.ExecuteActions(new DeviceAction("door", "door=1")),
}, prazo.Token);
// Um PushResult por comando, na mesma ordem; a falha de um não cancela os outros.
Qualquer endpoint da API pode ir via Push com new PushCommand("endpoint", body).
Sempre passe um CancellationToken com prazo: se o equipamento estiver offline, o comando fica na fila até ele aparecer.
push.GetLastSeen(deviceId) informa a última consulta de cada equipamento.
Sem ASP.NET Core, use MonitorNotifications.Parse(rota, corpo), PushServer.HandlePush(deviceId, uuid) e
PushServer.HandleResult(deviceId, uuid, corpo) nas rotas do seu servidor HTTP.
O sample samples/ControlId.Net.Server tem um servidor completo.
Modo online (Pro e Enterprise)
No modo online o equipamento pergunta ao seu servidor, a cada tentativa de acesso, se deve liberar:
- Pro: o equipamento identifica a pessoa e envia o ID do usuário (
UserIdentifiedEvent). - Enterprise: o equipamento envia a credencial crua e o servidor identifica: cartão, QR Code, tag UHF, ID e senha, imagem ou template da digital (
CardEvent,QrCodeEvent,UhfTagEvent,UserIdAndPasswordEvent,BiometricEvent).
using ControlId.Access.Online;
// No equipamento, uma vez: registra o servidor e liga o modo online.
await device.ConfigureOnlineModeAsync(new OnlineModeConfiguration
{
ServerAddress = "http://192.168.0.20:5000/api",
Mode = OnlineMode.Pro,
RequestTimeout = TimeSpan.FromSeconds(3), // máx. 5 s
MaxRequestAttempts = 3, // falhas antes da contingência
});
No servidor (pacote ControlId.Net.AspNetCore):
builder.Services.AddControlIdOnline<RegrasDeAcesso>();
app.MapControlIdOnline("/api"); // o mesmo caminho do ServerAddress
public sealed class RegrasDeAcesso(MeuDbContext db) : IOnlineAccessHandler
{
public async Task<OnlineAuthorization> AuthorizeAsync(OnlineIdentificationEvent tentativa, CancellationToken ct)
{
var pessoa = tentativa switch
{
UserIdentifiedEvent u => await db.Pessoas.FindAsync([u.UserId], ct),
CardEvent c => await db.Pessoas.FirstOrDefaultAsync(p => p.Cartao == c.CardValue, ct),
_ => null,
};
if (pessoa is null) return OnlineAuthorization.NotIdentified();
if (!pessoa.Ativa) return OnlineAuthorization.Deny(pessoa.Id, pessoa.Nome, "Cadastro inativo");
return OnlineAuthorization.Grant(pessoa.Id, pessoa.Nome, DeviceAction.OpenDoor(1));
// iDFlex/iDAccess Pro/Nano: DeviceAction.OpenSecBox(); iDBlock: DeviceAction.ReleaseTurnstile(...)
}
// Opcionais: botoeira, resultado de cadastro remoto e foto exibida no display (com UserImage = true na autorização).
public Task OnExitButtonAsync(ExitButtonEvent e, CancellationToken ct) => Task.CompletedTask;
public Task OnRemoteEnrollAsync(OnlineEnrollEvent e, CancellationToken ct) => Task.CompletedTask;
public Task<byte[]?> GetUserImageAsync(long userId, CancellationToken ct) => Task.FromResult<byte[]?>(null);
}
- O equipamento espera a resposta por no máximo
RequestTimeout(até 5 s). Exceção ou demora contam como falha; depois deMaxRequestAttemptsfalhas ele entra em contingência e decide sozinho com os dados locais, até o servidor voltar (device_is_alive, já respondido pelas rotas). client.AuthorizeRemotelyAsync(...)libera um acesso em tempo real a partir do servidor (remote_user_authorization).- Sem ASP.NET Core:
OnlineEvents.Parse(rota, campos, corpo)interpreta a requisição,authorization.ToResponseJson()gera a resposta eOnlineEvents.ParseEnroll(rota, json)lê os resultados de cadastro remoto.
REP iDClass
using ControlId.Rep;
// O REP usa HTTPS com certificado autoassinado; esse construtor já aceita o certificado.
using var rep = new RepClient("https://192.168.0.130", mode: RepMode.Portaria671);
var info = await rep.GetSystemInformationAsync();
string afdTexto = await rep.GetAfdFromNsrAsync(info.LastNsr - 100); // texto cru
var users = await rep.GetAllUsersAsync();
await rep.SetDateTimeAsync(DateTime.Now);
AFD tipado
O parser lê os leiautes da Portaria 671/2021 e da Portaria 1510/2009 (detecta sozinho, linha a linha):
using ControlId.Rep.Afd;
AfdFile afd = await rep.GetAfdFileFromNsrAsync(ultimoNsrLido + 1);
foreach (var marcacao in afd.Punches) // tipos 3 (REP-C/REP-A) e 7 (REP-P)
{
Console.WriteLine($"{marcacao.Nsr} {marcacao.Time:dd/MM/yyyy HH:mm} {marcacao.EmployeeId}"); // CPF (671) ou PIS (1510)
}
ultimoNsrLido = afd.LastNsr ?? ultimoNsrLido; // leitura incremental
- Também lê arquivos:
AfdParser.Parse(File.OpenRead("AFD.txt"))(detecta UTF-8 ou ISO-8859-1) ouAfdParser.ReadRecords(reader)para ler registro a registro, sem carregar o arquivo todo. - Disponíveis: cabeçalho (
Header), empresa (CompanyRecords), empregados (EmployeeRecords), ajustes de relógio (ClockAdjustments), eventos sensíveis (SensitiveEvents), trailer e assinatura. - Datas são o horário de parede gravado pelo REP; no leiaute 671 o fuso vem em
UtcOffset(eTimeWithOffset). - Uma linha corrompida não derruba a leitura: ela vira
AfdInvalidRecorde aparece emafd.Issues, junto com divergências do trailer e NSR fora de ordem. IsCrcValidconfere o CRC-16/KERMIT, o algoritmo que a portaria exige para REP-A e REP-P. Para REP-C a portaria não define o algoritmo, então trate o valor como informativo.- O
RepClientdecodifica o AFD como ISO-8859-1 quando o REP não informa o charset, para não corromper nomes acentuados.
Injeção de dependência
dotnet add package ControlId.Net.DependencyInjection --prerelease
Um equipamento só (fica injetável como IAccessClient / IRepClient):
builder.Services.AddControlIdAccess(o =>
{
o.BaseAddress = new Uri("http://192.168.0.129");
o.Password = builder.Configuration["ControlId:Senha"]!;
});
builder.Services.AddControlIdRep(o => o.BaseAddress = new Uri("https://192.168.0.130")); // aceita o certificado autoassinado
public class PortariaService(IAccessClient portaria) { ... }
Vários equipamentos, direto do appsettings.json (a chave vira o nome):
{
"ControlId": {
"Access": {
"portaria": { "BaseAddress": "http://192.168.0.129", "Password": "..." },
"garagem": { "BaseAddress": "http://192.168.0.131", "Timeout": "00:00:10" }
},
"Rep": {
"rh": { "BaseAddress": "https://192.168.0.130", "Mode": "Portaria671" }
}
}
}
builder.Services.AddControlIdAccessDevices(builder.Configuration.GetSection("ControlId:Access"));
builder.Services.AddControlIdRepDevices(builder.Configuration.GetSection("ControlId:Rep"));
public class Sincronizacao(IControlIdClientFactory equipamentos)
{
public async Task RodarAsync()
{
foreach (var nome in equipamentos.AccessDeviceNames)
{
var logs = await equipamentos.GetAccessClient(nome).GetAccessLogsAsync();
}
var afd = await equipamentos.GetRepClient("rh").GetAfdFileAsync();
}
}
- Cada equipamento tem um cliente único durante a vida da aplicação, então a sessão de login é reaproveitada. Não descarte os clientes: o container faz isso.
- O
HttpClientvem doIHttpClientFactory.AddControlIdAccess/AddControlIdRepdevolvem oIHttpClientBuilder, para encadear handlers (proxy, retry, logging). - Nome desconhecido ou configuração inválida geram
InvalidOperationExceptioncom o nome do equipamento. - O
ControlId.Net.AspNetCorejá inclui este pacote.
HttpClient externo
Sem o pacote de DI, os dois clientes também aceitam um HttpClient externo, que eles não descartam:
services.AddHttpClient("portaria-principal");
var http = factory.CreateClient("portaria-principal");
var device = new AccessClient(new ControlIdDeviceOptions
{
BaseAddress = new Uri("http://192.168.0.129"),
Login = "admin",
Password = "admin",
}, http);
Bases grandes e vários equipamentos
Limite por equipamento
Cada cliente limita as requisições simultâneas ao seu equipamento (MaxConcurrentRequests, padrão 4, login incluído).
Dá para disparar quantas chamadas quiser em paralelo: as que passarem do limite esperam a vez, sem sobrecarregar o
equipamento. Cada equipamento tem o próprio limite, então vários equipamentos rodam em paralelo entre si.
var device = new AccessClient(new ControlIdDeviceOptions
{
BaseAddress = new Uri("http://192.168.0.129"),
MaxConcurrentRequests = 2, // equipamento mais modesto ou rede lenta
});
var fotos = await Task.WhenAll(ids.Select(id => device.GetUserImageAsync(id))); // no máximo 2 por vez no equipamento
- O limite é o mesmo em qualquer runtime. Sem ele, o .NET Framework seguraria em 2 conexões por servidor e o .NET Core
não limitaria nada. Quando o pacote cria o
HttpClient(ou pelo pacote de DI), o limite de conexões do handler acompanha o valor. - Com um
HttpClientexterno, o limite do cliente continua valendo, mas o limite de conexões do handler fica por sua conta. - No
appsettings.json:"portaria": { "BaseAddress": "...", "MaxConcurrentRequests": 2 }.
Leitura paginada
GetUsersAsync e GetObjectsAsync trazem tudo numa requisição só. Em bases grandes, use os Enumerate*Async, que leem
em páginas (500 registros por padrão) e só buscam a próxima quando a anterior foi consumida:
await foreach (var usuario in device.EnumerateUsersAsync())
{
await SincronizarAsync(usuario);
}
// Coleta incremental: guarde o último ID e continue dele na próxima vez.
await foreach (var log in device.EnumerateAccessLogsAsync(afterId: ultimoId, pageSize: 1000))
{
ultimoId = log.Id;
}
await foreach (var cartao in device.EnumerateObjectsAsync<AccessCard>(Where.Equal("cards", "user_id", 42L))) { }
// Qualquer objeto: Offset é o ponto de partida e Limit, o total máximo.
var request = new LoadObjectsRequest("user_groups") { Order = new List<string> { "user_id" } };
await foreach (var linha in device.EnumerateObjectsAsync<JsonElement>(request, pageSize: 200)) { }
- Usuários e logs seguem pelo ID (
id > último): criar ou remover registros durante a leitura não faz pular nem repetir os demais. - Os outros objetos usam
limit/offset, ordenados pela chave do modelo. ComLoadObjectsRequest, informeOrderpara as páginas ficarem estáveis. - Sair do
await foreach(ou cancelar o token) para a leitura sem buscar as páginas restantes.
Vários equipamentos de uma vez
ControlIdDevices.RunAsync executa a mesma operação em vários equipamentos em paralelo (8 por vez, por padrão) e devolve
um resultado por equipamento, na ordem de entrada. Um equipamento fora do ar não interrompe os demais:
var equipamentos = new Dictionary<string, IAccessClient> { ["portaria"] = portaria, ["garagem"] = garagem };
var resultados = await ControlIdDevices.RunAsync(equipamentos, (d, ct) => d.CreateUserAsync(usuario, ct), maxParallelism: 4);
foreach (var r in resultados)
{
if (r.Succeeded) Console.WriteLine($"{r.Device}: criado com ID {r.Value} em {r.Elapsed.TotalMilliseconds} ms");
else Console.WriteLine($"{r.Device}: {r.Error!.Message}");
}
Com o pacote de DI, direto nos equipamentos registrados:
// Todos os equipamentos de acesso (ou só alguns: RunOnAccessDevicesAsync(new[] { "portaria", "garagem" }, ...)).
var logs = await equipamentos.RunOnAccessDevicesAsync((d, ct) => d.GetAccessLogsAsync(ultimoId, cancellationToken: ct));
var porta = await equipamentos.RunOnAccessDevicesAsync((d, ct) => d.OpenDoorAsync(1, ct)); // sem valor: DeviceResult
var afds = await equipamentos.RunOnRepDevicesAsync((r, ct) => r.GetAfdFileAsync(cancellationToken: ct));
r.Valuede um equipamento que falhou lançaInvalidOperationExceptioncom o erro original emInnerException; confiraSucceededantes.- Erros ao obter o cliente (ex.: equipamento registrado sem
BaseAddress) também ficam só no resultado daquele equipamento. - Cancelar o token lança
OperationCanceledExceptionem vez de devolver resultados parciais. maxParallelismlimita quantos equipamentos são atendidos ao mesmo tempo; dentro de cada um continua valendo oMaxConcurrentRequests.
Erros
| Exceção | Quando |
|---|---|
ControlIdAuthenticationException |
Usuário ou senha recusados no login.fcgi |
ControlIdException |
HTTP de erro do equipamento (com StatusCode, DeviceErrorCode e ResponseBody), timeout ou falha de rede |
Roadmap
- Transporte com sessão automática e re-login
- Acesso: objetos genéricos, usuários, logs, porta/SecBox/catraca, sistema, data/hora
- REP: about, sistema, data/hora, usuários, AFD
- Acesso: cartões, digitais, fotos, grupos, horários, portais e regras de acesso tipados
- Acesso: modo Push e Monitor (eventos em tempo real)
- Acesso: Push em lote (
transactions) e pacoteControlId.Net.AspNetCorecomMapControlIdPush/MapControlIdMonitor - REP: parser do AFD (Portaria 671 e legado) para objetos tipados
- Extensões para
IServiceCollection(AddControlIdAccess,AddControlIdRep) - Chamadas genéricas a qualquer endpoint (
SendAsync,SendRawAsync,SendBytesAsync,DownloadAsync) eget_configuration - Acesso: cadastro remoto (
remote_enroll) síncrono, assíncrono via Monitor e via Push - Acesso: modo online (Pro/Enterprise): configuração, eventos tipados,
MapControlIdOnlinee autorização remota - Acesso: comandos tipados (mensagem, buzzer, login, hash de senha, reinício, reset, rede, logotipo, GPIO, LED, USB, upsert de objetos)
- Acesso: cobertura completa da documentação pública (endpoints, objetos, eventos e configurações), com teste que garante cada endpoint (docs/COBERTURA.md)
- REP: levantamento de cobertura (depende do acesso à documentação do iDClass)
- Testes de integração contra equipamentos reais
- Outras marcas de controle de acesso, em pacotes separados
Desenvolvimento
dotnet build
dotnet test
dotnet run --project samples/ControlId.Net.Sample -- access http://192.168.0.129 admin admin
dotnet run --project samples/ControlId.Net.Server -- --urls http://0.0.0.0:5000
Os testes rodam em net8.0, net10.0 e net48. Os dois primeiros exercitam o build net8.0 das bibliotecas (o net10.0 garante a compatibilidade com o .NET 10, inclusive o pacote ASP.NET Core sobre o ASP.NET Core 10); o net48 exercita o build netstandard2.0, usado no .NET Framework, e só executa no Windows. Para compilar o alvo net10.0 é preciso o SDK do .NET 10. Em Linux ou macOS, use dotnet test -f net8.0 ou -f net10.0. No CI, os dois rodam no job Linux e o net48 num job Windows separado; o publish.yml só publica se todos passarem.
Para publicar, crie uma tag vX.Y.Z e faça push. O workflow publish.yml empacota ControlId.Net, ControlId.Net.DependencyInjection e ControlId.Net.AspNetCore com a versão da tag e envia ao NuGet.org por Trusted Publishing (OIDC), sem chave guardada no repositório. Configuração única: uma política de Trusted Publishing no nuget.org para este repositório e o arquivo publish.yml, e o secret NUGET_USER com o nome de usuário do nuget.org.
Licença
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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 was computed. 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. |
-
net8.0
- ControlId.Net.DependencyInjection (>= 0.4.1-alpha.1)
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 |
|---|---|---|
| 0.5.0-alpha.1 | 54 | 10/30/2026 |
| 0.4.3-alpha.1 | 59 | 10/30/2026 |
| 0.4.2-alpha.1 | 62 | 10/13/2026 |
| 0.4.1-alpha.1 | 60 | 10/13/2026 |
| 0.4.0-alpha.1 | 51 | 10/13/2026 |
| 0.3.0-alpha.1 | 47 | 10/13/2026 |
| 0.2.0-alpha.1 | 55 | 10/12/2026 |
| 0.1.0-alpha.2 | 59 | 10/11/2026 |
| 0.1.0-alpha.1 | 57 | 10/11/2026 |