Back to Blog
6 min read

Dataverse Web API: Building Enterprise Data Solutions

Microsoft Dataverse (formerly Common Data Service) provides a secure, scalable data platform for Power Platform and Dynamics 365. The Web API enables developers to interact with Dataverse programmatically.

Understanding Dataverse

Dataverse offers:

  • Relational data storage
  • Built-in security model
  • Business logic capabilities
  • Integration with Power Platform

Authentication Setup

Configure authentication for Dataverse:

using Azure.Identity;
using System.Net.Http.Headers;

public class DataverseClient
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl;

    public DataverseClient(string environmentUrl)
    {
        _baseUrl = $"{environmentUrl}/api/data/v9.2/";

        var credential = new DefaultAzureCredential();
        var token = credential.GetToken(
            new Azure.Core.TokenRequestContext(
                new[] { $"{environmentUrl}/.default" }));

        _httpClient = new HttpClient();
        _httpClient.BaseAddress = new Uri(_baseUrl);
        _httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token.Token);
        _httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
        _httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
        _httpClient.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
    }
}

CRUD Operations

Perform basic data operations:

public class AccountService
{
    private readonly HttpClient _client;

    public AccountService(HttpClient client)
    {
        _client = client;
    }

    // Create
    public async Task<Guid> CreateAccountAsync(Account account)
    {
        var json = JsonSerializer.Serialize(new
        {
            name = account.Name,
            telephone1 = account.Phone,
            emailaddress1 = account.Email,
            address1_city = account.City,
            revenue = account.Revenue,
            industrycode = account.IndustryCode
        });

        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var response = await _client.PostAsync("accounts", content);

        response.EnsureSuccessStatusCode();

        // Get created record ID from header
        var entityUrl = response.Headers.GetValues("OData-EntityId").First();
        var idMatch = Regex.Match(entityUrl, @"\(([^)]+)\)");
        return Guid.Parse(idMatch.Groups[1].Value);
    }

    // Read single
    public async Task<Account?> GetAccountAsync(Guid accountId)
    {
        var response = await _client.GetAsync(
            $"accounts({accountId})?$select=name,telephone1,emailaddress1,revenue");

        if (response.StatusCode == HttpStatusCode.NotFound)
            return null;

        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        var data = JsonSerializer.Deserialize<JsonElement>(json);

        return new Account
        {
            Id = accountId,
            Name = data.GetProperty("name").GetString(),
            Phone = data.GetProperty("telephone1").GetString(),
            Email = data.GetProperty("emailaddress1").GetString(),
            Revenue = data.GetProperty("revenue").GetDecimal()
        };
    }

    // Read multiple with filter
    public async Task<List<Account>> GetAccountsAsync(string? nameFilter = null, int top = 50)
    {
        var query = $"accounts?$select=accountid,name,telephone1,emailaddress1,revenue&$top={top}";

        if (!string.IsNullOrEmpty(nameFilter))
        {
            query += $"&$filter=contains(name,'{nameFilter}')";
        }

        query += "&$orderby=name asc";

        var response = await _client.GetAsync(query);
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        var data = JsonSerializer.Deserialize<JsonElement>(json);

        var accounts = new List<Account>();
        foreach (var item in data.GetProperty("value").EnumerateArray())
        {
            accounts.Add(new Account
            {
                Id = Guid.Parse(item.GetProperty("accountid").GetString()!),
                Name = item.GetProperty("name").GetString(),
                Phone = item.TryGetProperty("telephone1", out var phone) ? phone.GetString() : null,
                Email = item.TryGetProperty("emailaddress1", out var email) ? email.GetString() : null,
                Revenue = item.TryGetProperty("revenue", out var rev) ? rev.GetDecimal() : 0
            });
        }

        return accounts;
    }

    // Update
    public async Task UpdateAccountAsync(Guid accountId, Account account)
    {
        var json = JsonSerializer.Serialize(new
        {
            name = account.Name,
            telephone1 = account.Phone,
            emailaddress1 = account.Email,
            revenue = account.Revenue
        });

        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var request = new HttpRequestMessage(HttpMethod.Patch, $"accounts({accountId})")
        {
            Content = content
        };

        var response = await _client.SendAsync(request);
        response.EnsureSuccessStatusCode();
    }

    // Delete
    public async Task DeleteAccountAsync(Guid accountId)
    {
        var response = await _client.DeleteAsync($"accounts({accountId})");
        response.EnsureSuccessStatusCode();
    }
}

public record Account
{
    public Guid Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public string? Phone { get; init; }
    public string? Email { get; init; }
    public string? City { get; init; }
    public decimal Revenue { get; init; }
    public int? IndustryCode { get; init; }
}

Working with Relationships

Handle related records:

public class RelationshipService
{
    private readonly HttpClient _client;

    public RelationshipService(HttpClient client)
    {
        _client = client;
    }

    // Create contact with account relationship
    public async Task<Guid> CreateContactWithAccountAsync(Contact contact, Guid accountId)
    {
        var json = JsonSerializer.Serialize(new
        {
            firstname = contact.FirstName,
            lastname = contact.LastName,
            emailaddress1 = contact.Email,
            // Reference to parent account
            @"parentcustomerid_account@odata.bind" = $"/accounts({accountId})"
        });

        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var response = await _client.PostAsync("contacts", content);
        response.EnsureSuccessStatusCode();

        var entityUrl = response.Headers.GetValues("OData-EntityId").First();
        var idMatch = Regex.Match(entityUrl, @"\(([^)]+)\)");
        return Guid.Parse(idMatch.Groups[1].Value);
    }

    // Get account with related contacts
    public async Task<AccountWithContacts> GetAccountWithContactsAsync(Guid accountId)
    {
        var query = $"accounts({accountId})?" +
            "$select=name,telephone1,revenue" +
            "&$expand=contact_customer_accounts($select=contactid,firstname,lastname,emailaddress1)";

        var response = await _client.GetAsync(query);
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        var data = JsonSerializer.Deserialize<JsonElement>(json);

        var account = new AccountWithContacts
        {
            Id = accountId,
            Name = data.GetProperty("name").GetString()!,
            Revenue = data.GetProperty("revenue").GetDecimal(),
            Contacts = new List<Contact>()
        };

        if (data.TryGetProperty("contact_customer_accounts", out var contacts))
        {
            foreach (var contact in contacts.EnumerateArray())
            {
                account.Contacts.Add(new Contact
                {
                    Id = Guid.Parse(contact.GetProperty("contactid").GetString()!),
                    FirstName = contact.GetProperty("firstname").GetString(),
                    LastName = contact.GetProperty("lastname").GetString(),
                    Email = contact.GetProperty("emailaddress1").GetString()
                });
            }
        }

        return account;
    }

    // Associate records
    public async Task AssociateContactToAccountAsync(Guid contactId, Guid accountId)
    {
        var json = JsonSerializer.Serialize(new
        {
            @"@odata.id" = $"{_client.BaseAddress}accounts({accountId})"
        });

        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var response = await _client.PostAsync(
            $"contacts({contactId})/parentcustomerid_account/$ref",
            content);

        response.EnsureSuccessStatusCode();
    }
}

public record Contact
{
    public Guid Id { get; init; }
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
    public string? Email { get; init; }
}

public record AccountWithContacts : Account
{
    public List<Contact> Contacts { get; init; } = new();
}

Batch Operations

Execute multiple operations in a single request:

public class BatchService
{
    private readonly HttpClient _client;

    public BatchService(HttpClient client)
    {
        _client = client;
    }

    public async Task ExecuteBatchAsync()
    {
        var batchId = Guid.NewGuid().ToString();
        var changesetId = Guid.NewGuid().ToString();

        var batchContent = new StringBuilder();

        // Batch boundary start
        batchContent.AppendLine($"--batch_{batchId}");
        batchContent.AppendLine($"Content-Type: multipart/mixed; boundary=changeset_{changesetId}");
        batchContent.AppendLine();

        // Changeset item 1: Create account
        batchContent.AppendLine($"--changeset_{changesetId}");
        batchContent.AppendLine("Content-Type: application/http");
        batchContent.AppendLine("Content-Transfer-Encoding: binary");
        batchContent.AppendLine("Content-ID: 1");
        batchContent.AppendLine();
        batchContent.AppendLine("POST accounts HTTP/1.1");
        batchContent.AppendLine("Content-Type: application/json");
        batchContent.AppendLine();
        batchContent.AppendLine(JsonSerializer.Serialize(new { name = "New Account 1", revenue = 100000 }));

        // Changeset item 2: Create another account
        batchContent.AppendLine($"--changeset_{changesetId}");
        batchContent.AppendLine("Content-Type: application/http");
        batchContent.AppendLine("Content-Transfer-Encoding: binary");
        batchContent.AppendLine("Content-ID: 2");
        batchContent.AppendLine();
        batchContent.AppendLine("POST accounts HTTP/1.1");
        batchContent.AppendLine("Content-Type: application/json");
        batchContent.AppendLine();
        batchContent.AppendLine(JsonSerializer.Serialize(new { name = "New Account 2", revenue = 200000 }));

        // Close changeset
        batchContent.AppendLine($"--changeset_{changesetId}--");

        // Close batch
        batchContent.AppendLine($"--batch_{batchId}--");

        var content = new StringContent(
            batchContent.ToString(),
            Encoding.UTF8,
            $"multipart/mixed; boundary=batch_{batchId}");

        var response = await _client.PostAsync("$batch", content);
        response.EnsureSuccessStatusCode();

        var result = await response.Content.ReadAsStringAsync();
        Console.WriteLine(result);
    }
}

FetchXML Queries

Use FetchXML for complex queries:

public class FetchXmlService
{
    private readonly HttpClient _client;

    public FetchXmlService(HttpClient client)
    {
        _client = client;
    }

    public async Task<List<AccountSummary>> GetTopAccountsByRevenueAsync(int top = 10)
    {
        var fetchXml = $@"
            <fetch top='{top}'>
                <entity name='account'>
                    <attribute name='accountid' />
                    <attribute name='name' />
                    <attribute name='revenue' />
                    <attribute name='industrycode' />
                    <order attribute='revenue' descending='true' />
                    <filter type='and'>
                        <condition attribute='statecode' operator='eq' value='0' />
                        <condition attribute='revenue' operator='not-null' />
                    </filter>
                </entity>
            </fetch>";

        var encodedFetch = Uri.EscapeDataString(fetchXml);
        var response = await _client.GetAsync($"accounts?fetchXml={encodedFetch}");
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        var data = JsonSerializer.Deserialize<JsonElement>(json);

        var accounts = new List<AccountSummary>();
        foreach (var item in data.GetProperty("value").EnumerateArray())
        {
            accounts.Add(new AccountSummary
            {
                Id = Guid.Parse(item.GetProperty("accountid").GetString()!),
                Name = item.GetProperty("name").GetString()!,
                Revenue = item.GetProperty("revenue").GetDecimal()
            });
        }

        return accounts;
    }

    public async Task<List<ContactsByIndustry>> GetContactsGroupedByIndustryAsync()
    {
        var fetchXml = @"
            <fetch aggregate='true'>
                <entity name='contact'>
                    <attribute name='contactid' alias='contact_count' aggregate='count' />
                    <link-entity name='account' from='accountid' to='parentcustomerid' alias='acc'>
                        <attribute name='industrycode' alias='industry' groupby='true' />
                    </link-entity>
                </entity>
            </fetch>";

        var encodedFetch = Uri.EscapeDataString(fetchXml);
        var response = await _client.GetAsync($"contacts?fetchXml={encodedFetch}");
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        var data = JsonSerializer.Deserialize<JsonElement>(json);

        var results = new List<ContactsByIndustry>();
        foreach (var item in data.GetProperty("value").EnumerateArray())
        {
            results.Add(new ContactsByIndustry
            {
                IndustryCode = item.GetProperty("industry").GetInt32(),
                ContactCount = item.GetProperty("contact_count").GetInt32()
            });
        }

        return results;
    }
}

public record AccountSummary
{
    public Guid Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public decimal Revenue { get; init; }
}

public record ContactsByIndustry
{
    public int IndustryCode { get; init; }
    public int ContactCount { get; init; }
}

Summary

Dataverse Web API provides:

  • RESTful access to Dataverse data
  • OData query capabilities
  • Batch operations for efficiency
  • FetchXML for complex queries
  • Relationship management

Build enterprise-grade applications with Microsoft’s data platform.


References:

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.