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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="OData.Mapper" Version="1.0.0.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OData.Mapper" Version="1.0.0.7" />
                    
Directory.Packages.props
<PackageReference Include="OData.Mapper" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add OData.Mapper --version 1.0.0.7
                    
#r "nuget: OData.Mapper, 1.0.0.7"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package OData.Mapper@1.0.0.7
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=OData.Mapper&version=1.0.0.7
                    
Install as a Cake Addin
#tool nuget:?package=OData.Mapper&version=1.0.0.7
                    
Install as a Cake Tool

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.Json e 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
}
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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
1.0.0.7 108 7/15/2026
1.0.0.6 98 7/15/2026
1.0.0.5 103 7/14/2026
1.0.0.4 110 7/14/2026
1.0.0.3 98 7/14/2026
1.0.0.2 112 7/14/2026