Creating Microsoft Dataverse Tables and Columns Programmatically with C# and the SDK

Power Platform Engineering Academy | APP-001 — Dataverse Metadata Explorer

Technical focus: C#, Dataverse SDK, metadata operations, Microsoft Entra ID authentication, solution-aware customization, and schema deployment.

1. Introduction

Microsoft Dataverse is more than a relational data store. It is a metadata-driven application platform where tables, columns, relationships, security definitions, and application components are described through platform metadata.

Although the Power Apps Maker Portal provides a graphical interface for creating these components, Dataverse also exposes programmatic metadata operations through its SDK and Web API.

This article explores how to create a custom Dataverse table and its columns using C#, with all implementation logic contained in a single Program.cs file.

The objective is to understand how Dataverse schema creation works internally, how metadata requests are executed, and how to make a basic provisioning program safe to rerun.

This experiment forms the foundation of APP-001, the Dataverse Metadata Explorer, in the Power Platform Engineering Academy.

2. Experiment objectives

Our C# program will:

  1. Authenticate to Dataverse using Microsoft Entra ID and interactive OAuth.
  2. Verify connectivity with WhoAmIRequest.
  3. Determine whether a custom table already exists.
  4. Create the table and its primary name column.
  5. Create five additional columns with different metadata types.
  6. Associate the customizations with an unmanaged Dataverse solution.
  7. Publish the customizations.
  8. Retrieve and inspect the resulting table metadata.

The program uses the Microsoft.PowerPlatform.Dataverse.Client NuGet package.

Development environment

ComponentConfiguration
IDEVisual Studio
LanguageC#
Project typeConsole App
Framework.NET 10
NuGet packageMicrosoft.PowerPlatform.Dataverse.Client
AuthenticationMicrosoft Entra ID / OAuth
Dataverse solutionPEADataverse
Publisher prefixpea
Table logical namepea_experiment

The environment URL is anonymized throughout this article.

3. Understanding Dataverse metadata

A Dataverse table is described by an EntityMetadata object.

Despite the platform’s modern terminology of tables and columns, many SDK types still use the historical terms Entity and Attribute.

For example:

Modern terminologySDK terminology
TableEntity
ColumnAttribute
Table definitionEntityMetadata
Column definitionAttributeMetadata
Create tableCreateEntityRequest
Create columnCreateAttributeRequest

This distinction is important when reading Microsoft documentation, examining SDK classes, or debugging Dataverse customization operations.

A metadata operation modifies the structure of Dataverse. It is different from creating or updating business records using service.Create() or service.Update().

4. Connecting to Dataverse

We begin by importing the required namespaces:

using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata;
using Microsoft.Crm.Sdk.Messages;

The ServiceClient class provides an authenticated client for interacting with Dataverse.

Our local learning experiment uses an OAuth connection string:

string environmentUrl =
"https://YOUR-ORGANIZATION.crm.dynamics.com";
string connectionString =
$"AuthType=OAuth;" +
$"Url={environmentUrl};" +
"AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;" +
"RedirectUri=http://localhost;" +
"LoginPrompt=Always;";

The application ID shown here is a Microsoft-documented development sample. Enterprise applications should use an appropriately configured Microsoft Entra application registration.

We deliberately avoid embedding usernames, passwords, tokens, or client secrets in the source code.

The connection is established as follows:

using var service = new ServiceClient(connectionString);
if (!service.IsReady)
{
throw new InvalidOperationException(
service.LastError ?? "Dataverse connection failed.");
}

Next, we execute a simple organization request:

var whoAmI =
(WhoAmIResponse)service.Execute(new WhoAmIRequest());
Console.WriteLine($"User ID: {whoAmI.UserId}");

WhoAmIRequest returns information about the identity under which Dataverse is executing the request.

A successful response demonstrates that the application can authenticate and communicate with Dataverse.

It does not establish that the user has privileges to customize the environment. Metadata creation requires additional authorization.

Experiment evidence: The SDK installation and WhoAmIRequest authentication test were successfully executed during APP-001 development.

5. Creating a Dataverse table

The principal SDK operation is CreateEntityRequest.

This request accepts two important metadata objects:

  • EntityMetadata, which describes the table.
  • StringAttributeMetadata, which describes its primary name column.

Our implementation is:

var createTable = new CreateEntityRequest
{
SolutionUniqueName = "PEADataverse",
Entity = new EntityMetadata
{
SchemaName = "pea_Experiment",
DisplayName = new Label("Experiment", 1033),
DisplayCollectionName =
new Label("Experiments", 1033),
OwnershipType = OwnershipTypes.UserOwned,
IsActivity = false
},
PrimaryAttribute = new StringAttributeMetadata
{
SchemaName = "pea_Name",
DisplayName = new Label("Name", 1033),
MaxLength = 200,
FormatName = StringFormatName.Text
}
};
service.Execute(createTable);

Understanding the properties

SchemaName

The schema name identifies the component in Dataverse metadata. It incorporates the publisher’s customization prefix.

For example:

pea_Experiment

DisplayName

The human-readable name presented in the application interface.

DisplayCollectionName

The plural display name used when representing collections of records.

OwnershipType

Determines the ownership model for the table.

In this experiment, OwnershipTypes.UserOwned enables user/team ownership, which is relevant to Dataverse record-level security.

PrimaryAttribute

Defines the primary name column.

The primary name column is created as part of the table-creation request, rather than through a separate CreateAttributeRequest.

Why solution association matters

The SolutionUniqueName property instructs Dataverse to associate the newly created customization with a specified unmanaged solution.

This matters for application lifecycle management because customizations must be packaged and transported consistently between environments.

However, solution membership alone does not constitute a complete ALM strategy. Dependencies, deployment order, environment configuration, versioning, and validation must also be addressed.

6. Creating columns using metadata classes

Dataverse exposes specialized metadata classes for different column types.

Our Experiment table uses five additional columns.

ColumnMetadata class
Application CodeStringAttributeMetadata
DescriptionMemoAttributeMetadata
StatusPicklistAttributeMetadata
Last ExecutionDateTimeAttributeMetadata
Execution CountIntegerAttributeMetadata

Example: Text column

var applicationCode = new StringAttributeMetadata
{
SchemaName = "pea_ApplicationCode",
DisplayName = new Label("Application Code", 1033),
MaxLength = 30
};

The metadata definition is submitted through CreateAttributeRequest:

service.Execute(new CreateAttributeRequest
{
EntityName = "pea_experiment",
Attribute = applicationCode,
SolutionUniqueName = "PEADataverse"
});

The same request pattern is reused for other supported attribute metadata types.

Example: Choice column

Choice columns require an OptionSetMetadata definition.

var status = new PicklistAttributeMetadata
{
SchemaName = "pea_Status",
DisplayName = new Label("Status", 1033),
OptionSet = new OptionSetMetadata
{
IsGlobal = false,
OptionSetType = OptionSetType.Picklist,
Options =
{
new OptionMetadata(
new Label("Planned", 1033), 100000000),
new OptionMetadata(
new Label("In Progress", 1033), 100000001),
new OptionMetadata(
new Label("Completed", 1033), 100000002),
new OptionMetadata(
new Label("Failed", 1033), 100000003)
}
}
};

Here, IsGlobal = false indicates that the choice definition is local to the column.

The option values are explicitly assigned by the program rather than generated automatically.

This introduces an important architectural consideration: integer option values become part of the data contract and should be managed consistently across environments.

7. Making schema creation repeatable

A provisioning program should not blindly attempt to create components that already exist.

Our initial implementation uses RetrieveAllEntitiesRequest to retrieve table definitions:

var response =
(RetrieveAllEntitiesResponse)service.Execute(
new RetrieveAllEntitiesRequest
{
EntityFilters = EntityFilters.Entity,
RetrieveAsIfPublished = true
});

We then check for the table:

bool tableExists = response.EntityMetadata.Any(
entity => entity.LogicalName == "pea_experiment");

If the table already exists, creation is skipped.

We apply a similar technique to columns, retrieving existing attribute metadata and comparing logical names before calling CreateAttributeRequest.

Idempotency and its limitations

This approach supports basic create-if-missing behavior.

However, it is not a complete declarative schema reconciliation engine.

For example, if pea_applicationcode already exists with a maximum length of 20 rather than 30, the program will skip it instead of modifying the definition.

A more mature provisioning tool should distinguish:

  • Component missing: create it.
  • Component exists with expected metadata: leave it unchanged.
  • Component exists with compatible differences: evaluate an update.
  • Component exists with incompatible differences: report a schema conflict.

Some metadata changes have restrictions and cannot be safely treated as ordinary updates.

Performance consideration

RetrieveAllEntitiesRequest can retrieve metadata for every table in the environment.

That is useful for demonstrating metadata discovery, but it can be unnecessarily expensive when the application only needs to inspect one known table.

For targeted operations, RetrieveEntityRequest is generally a more appropriate choice.

This gives us a valuable follow-up experiment: compare metadata retrieval scope, response sizes, and execution times.

8. Publishing customizations

Creating metadata and publishing customizations are distinct operations.

Our implementation publishes the Experiment table through PublishXmlRequest:

service.Execute(new PublishXmlRequest
{
ParameterXml =
"<importexportxml><entities>" +
"<entity>pea_experiment</entity>" +
"</entities></importexportxml>"
});

This requests publication of the specified table’s customizations.

Targeted publishing is preferable to indiscriminately publishing every unpublished customization when working in shared development environments.

Publishing does not replace functional validation. The resulting table, columns, permissions, and application behavior must still be inspected.

9. Retrieving the final metadata

After publishing, we retrieve the table definition:

var verified =
(RetrieveEntityResponse)service.Execute(
new RetrieveEntityRequest
{
LogicalName = "pea_experiment",
EntityFilters =
EntityFilters.Entity |
EntityFilters.Attributes,
RetrieveAsIfPublished = false
});

We can now inspect important metadata properties:

var entity = verified.EntityMetadata;
Console.WriteLine(entity.SchemaName);
Console.WriteLine(entity.LogicalName);
Console.WriteLine(entity.EntitySetName);
Console.WriteLine(entity.PrimaryNameAttribute);

These properties expose different aspects of the table’s identity.

PropertyPurpose
SchemaNameSchema identifier
LogicalNameLogical identifier used in SDK operations
EntitySetNameCollection name used by the Dataverse Web API
PrimaryNameAttributeLogical name of the primary name column

Developers should retrieve the actual EntitySetName rather than assume that it can always be constructed by adding an s to the logical name.

This is especially relevant when building REST integrations.

10. Error handling and failure scenarios

Metadata operations can fail for several reasons.

Failure scenarioEngineering consideration
Authentication rejectedEntra ID configuration, Conditional Access, application registration
Missing customization privilegesDataverse security roles and permissions
Incorrect solution nameSolutionUniqueName must match the existing solution
Invalid schema namePublisher prefix and metadata naming rules
Existing incompatible columnSchema conflict requires explicit handling
Publishing failureInspect the organization service fault
Network interruptionDetermine which operations completed before retrying

Our initial program uses a top-level exception handler:

catch (Exception ex)
{
Console.Error.WriteLine(ex);
Environment.ExitCode = 1;
}

This is useful during development because it preserves the exception details.

For production tooling, structured diagnostics, correlation identifiers, fault-code inspection, and explicit retry policies deserve further consideration.

Importantly, multiple metadata requests do not automatically form one atomic transaction. If a later operation fails, earlier successful schema changes may remain.

This is why existence checks, inspection, and safe reruns are essential.

11. Engineering conclusions

This experiment demonstrates several fundamental principles of Dataverse development.

First, Dataverse schema definitions are accessible through APIs. Tables and columns can be created and inspected programmatically rather than exclusively through the Maker Portal.

Second, metadata is a first-class architectural concern. Understanding metadata classes, naming conventions, ownership, and solution membership is essential for reliable Dataverse engineering.

Third, repeatability requires deliberate design. A script that succeeds once is not necessarily a safe deployment tool. Existing components, partial failures, schema conflicts, and publishing behavior must be considered.

Fourth, authentication and authorization are different. A successful WhoAmIRequest confirms authenticated SDK access, not unrestricted customization privileges.

Finally, SDK-based schema creation provides an excellent foundation for exploring ALM, metadata discovery, and automated Dataverse provisioning.

Experiment status

The following evidence is currently available:

VerificationStatus
Console App compilationVerified
Dataverse SDK installationVerified
Interactive OAuth authenticationVerified
WhoAmIRequest executionVerified
Table and column creationAwaiting execution evidence
Metadata retrieval after creationAwaiting execution evidence
Model-Driven App and dashboardNot yet implemented

The implementation is therefore a reproducible engineering experiment, not yet a fully validated deployment.

12. Next engineering milestone

The next milestone is to execute the complete Program.cs, inspect the generated metadata in Dataverse, and confirm that the table and columns belong to the intended solution.

Once validated, APP-001 will continue with its Execution Log table, Model-Driven App, entry dashboard, and end-to-end testing.

That work will support a second article focused on the complete Dataverse Metadata Explorer application.

References

  1. Microsoft Learn — Create tables using code
  2. Microsoft Learn — Query table definitions
  3. Microsoft Learn — CreateEntityRequest
  4. Microsoft Learn — CreateAttributeRequest
  5. Microsoft Learn — Authenticate with OAuth
  6. Microsoft Learn — Work with solutions using APIs

Power Platform Engineering Academy — APP-001 | C# Technical Article 001

Engineering mastery through reproducible experiments.

Edvaldo Guimrães Filho Avatar

Published by