OData.Mapper
1.0.0.7
dotnet add package OData.Mapper --version 1.0.0.7
NuGet\Install-Package OData.Mapper -Version 1.0.0.7
<PackageReference Include="OData.Mapper" Version="1.0.0.7" />
<PackageVersion Include="OData.Mapper" Version="1.0.0.7" />
<PackageReference Include="OData.Mapper" />
paket add OData.Mapper --version 1.0.0.7
#r "nuget: OData.Mapper, 1.0.0.7"
#:package OData.Mapper@1.0.0.7
#addin nuget:?package=OData.Mapper&version=1.0.0.7
#tool nuget:?package=OData.Mapper&version=1.0.0.7
OData.Mapper
Biblioteca genérica e performática para .NET que converte respostas JSON de serviços OData v4 (incluindo Dataverse / Dynamics 365 Web API) em modelos fortemente tipados — e também serializa modelos de volta para payloads de criação/atualização, com geração automática de @odata.bind para lookups.
Não usa reflection "pura" no caminho quente: getters, setters, construtores e o método Add() de coleções são compilados uma única vez por tipo via Expression Trees e reaproveitados em cache, evitando o custo de PropertyInfo.SetValue/Activator.CreateInstance/MethodInfo.Invoke a cada mapeamento.
Instalação
dotnet add package OData.Mapper
Compatível com .NET 8 e .NET 9 (pacote multi-target).
Por que usar
- Performático: accessors compilados via Expression Trees, com cache por tipo.
- Genérico: um único mapper para qualquer entidade, sem gerar código por classe.
- Completo: cobre os tipos Edm mais comuns, metadados do OData, paginação, Open Types e o caminho de escrita.
- Sem dependências externas: usa apenas
System.Text.Jsone a BCL.
Modelo de exemplo
Todos os exemplos abaixo usam este modelo:
using System.Text.Json;
using System.Text.Json.Serialization;
using OData.Mapping;
public sealed class Contact
{
[JsonPropertyName("contactid")]
public Guid Id { get; set; }
[JsonPropertyName("fullname")]
public string? FullName { get; set; }
// Lookup para outra entidade (accounts). Só afeta a ESCRITA (ToJson) —
// na leitura o valor já chega pronto como Guid simples.
[JsonPropertyName("_parentcustomerid_value")]
[ODataLookup("accounts")]
public Guid? ParentAccountId { get; set; }
[JsonPropertyName("_parentcustomerid_value_formatted")]
public string? ParentAccountName { get; set; }
// Recolhe qualquer campo do JSON que não bata com nenhuma propriedade
// conhecida acima (funciona nas duas direções: leitura e escrita).
[ODataExtensionData]
public Dictionary<string, JsonElement>? ExtraData { get; set; }
}
Leitura (JSON do OData → modelo)
Um único registro
string json = await httpClient.GetStringAsync("https://.../contacts(3f2504e0-...)");
Contact contact = ODataMapper.ToModel<Contact>(json);
Coleção
string json = await httpClient.GetStringAsync("https://.../contacts?$select=fullname");
List<Contact> contacts = ODataMapper.ToModels<Contact>(json);
Coleção "lazy" (item a item, sem materializar tudo de uma vez)
using JsonDocument doc = JsonDocument.Parse(json);
foreach (Contact contact in ODataMapper.ToModelsLazy<Contact>(doc.RootElement))
{
Console.WriteLine(contact.FullName);
if (contact.FullName == "Ana") break; // não processa o resto
}
Com metadados de paginação (@odata.count, @odata.nextLink, @odata.deltaLink)
string json = await httpClient.GetStringAsync("https://.../contacts?$count=true&$top=50");
ODataResult<Contact> result = ODataMapper.ToResult<Contact>(json);
Console.WriteLine($"Total: {result.Count}");
foreach (Contact c in result.Value) { /* ... */ }
if (result.NextLink is not null)
{
string nextJson = await httpClient.GetStringAsync(result.NextLink);
// repete o processo para a próxima página
}
Direto de um Stream (sem materializar a string inteira antes)
using HttpResponseMessage response = await httpClient.GetAsync("https://.../contacts");
await using Stream stream = await response.Content.ReadAsStreamAsync();
List<Contact> contacts = await ODataMapper.ToModelsAsync<Contact>(stream, cancellationToken);
Também existem ToModelAsync<T> e ToResultAsync<T> equivalentes para os outros cenários.
Já tem um JsonElement?
Todos os métodos acima têm uma sobrecarga que aceita JsonElement em vez de string, para quando você já tem o parse feito (ex.: dentro de um JsonDocument maior, ou em testes):
using JsonDocument doc = JsonDocument.Parse(json);
Contact contact = ODataMapper.ToModel<Contact>(doc.RootElement);
Escrita (modelo → JSON do OData)
Serializa um modelo para o payload esperado em POST/PATCH. Propriedades nulas não são incluídas (payload parcial, ideal para PATCH), e propriedades marcadas com [ODataLookup] viram @odata.bind automaticamente.
var novoContato = new Contact
{
FullName = "Ana Silva",
ParentAccountId = accountGuid
};
string payload = ODataMapper.ToJson(novoContato);
// {"fullname":"Ana Silva","_parentcustomerid_value@odata.bind":"/accounts(guid-aqui)"}
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
await httpClient.PostAsync("https://.../contacts", content);
Use indented: true para formatar com quebras de linha (útil em debug/log):
string payloadFormatado = ODataMapper.ToJson(novoContato, indented: true);
Atributos
| Atributo | Leitura (ToModel/ToModels/ToResult) |
Escrita (ToJson) |
|---|---|---|
[ODataLookup("entitySet")] |
Ignorado, sem efeito | Gera "campo@odata.bind": "/entitySet(guid)" |
[ODataExtensionData] |
Recolhe propriedades do JSON não mapeadas | Espalha o dicionário de volta como propriedades soltas |
[ODataLookup]
Use em propriedades Guid/Guid? que representam uma referência (lookup/navigation) para outra entidade — e apenas se você vai usar o modelo para escrever (ToJson). Na leitura, o valor do lookup já chega pronto como Guid simples em _campo_value.
[ODataExtensionData]
Marque no máximo uma propriedade por classe, do tipo Dictionary<string, JsonElement>, quando precisar preservar campos do JSON que seu modelo não conhece (ex.: entidade dinâmica/Open Type, proxy de dados, endpoint genérico). Se você já sabe quais campos precisa e mapeou só esses, não é necessário usar esse atributo — os demais campos são descartados de propósito, e isso é o comportamento correto.
Metadados do OData suportados automaticamente
Se o modelo tiver propriedades com os sufixos abaixo (correspondendo ao nome de uma outra propriedade já mapeada), elas são preenchidas automaticamente a partir das anotações do OData:
| Sufixo na propriedade | Anotação de origem no JSON |
|---|---|
..._formatted |
X@OData.Community.Display.V1.FormattedValue |
..._logicalname |
X@Microsoft.Dynamics.CRM.lookuplogicalname |
..._navigation |
X@Microsoft.Dynamics.CRM.associatednavigationproperty |
Também são reconhecidos @odata.etag, @odata.context e @odata.type, caso o modelo tenha propriedades com esses nomes exatos.
Tipos suportados
string, Guid, bool, char, todos os inteiros e decimais (int, long, short, byte, sbyte, uint, ulong, ushort, decimal, double, float), DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan (Edm.Duration ISO-8601), byte[] (Edm.Binary em base64), enum (numérico ou string, incluindo o formato legado "Namespace.Tipo'Valor'"), arrays, List<T>, HashSet<T>, ICollection<T>/IEnumerable<T>/IList<T>/IReadOnly*<T> e qualquer coleção concreta com construtor sem parâmetros e método Add(T), além de objetos complexos aninhados (mapeados recursivamente).
Tipos numéricos também aceitam o valor vindo como string no JSON (alguns serviços OData enviam Edm.Int64 como string para evitar perda de precisão em clientes JavaScript).
Tratamento de erros
Falhas de conversão lançam ODataMappingException, com PropertyName e DestinationType preenchidos para facilitar o diagnóstico:
try
{
var contact = ODataMapper.ToModel<Contact>(json);
}
catch (ODataMappingException ex)
{
Console.WriteLine($"Falha ao mapear '{ex.PropertyName}' para '{ex.DestinationType?.Name}': {ex.Message}");
}
Requisitos
- .NET 8 ou .NET 9
System.Text.Json(já incluso no SDK)
Licença
MIT
| 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 is compatible. 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
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.