OData V4 supports open types - entity types that can have dynamic properties not defined in the schema. This allows entities to store additional key-value pairs beyond their declared properties.
- Overview
- Defining Open Type Entities
- Reading Dynamic Properties
- Writing Dynamic Properties
- Type Conversions
- Complete Example
Open types allow entities to include properties not declared in the metadata. Common use cases:
- Custom fields added by users
- Extension attributes
- Dynamic metadata
- Tenant-specific properties
Inherit from ODataOpenType to support dynamic properties:
using PanoramicData.OData.Client;
public class Contact : ODataOpenType
{
// Declared properties
public int Id { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
// Dynamic properties are handled by the base class
}var contact = await client.GetByKeyAsync<Contact, int>(1);
if (contact.HasDynamicProperty("CustomField1"))
{
Console.WriteLine("CustomField1 exists!");
}var contact = await client.GetByKeyAsync<Contact, int>(1);
var customValue = contact.GetDynamicString("CustomField1");
Console.WriteLine($"Custom field: {customValue}");// Integer
int? count = contact.GetDynamicInt("ViewCount");
// Boolean
bool? isActive = contact.GetDynamicBool("IsActive");
// Decimal/Double
decimal? rating = contact.GetDynamicDecimal("Rating");
double? score = contact.GetDynamicDouble("Score");
// DateTime
DateTime? createdAt = contact.GetDynamicDateTime("CustomCreatedAt");
DateTimeOffset? modifiedAt = contact.GetDynamicDateTimeOffset("CustomModifiedAt");
// Guid
Guid? externalId = contact.GetDynamicGuid("ExternalSystemId");// Complex object stored as dynamic property
var address = contact.GetDynamicProperty<Address>("CustomAddress");
if (address != null)
{
Console.WriteLine($"Street: {address.Street}");
Console.WriteLine($"City: {address.City}");
}var contact = await client.GetByKeyAsync<Contact, int>(1);
foreach (var propertyName in contact.GetDynamicPropertyNames())
{
Console.WriteLine($"Dynamic property: {propertyName}");
}var contact = new Contact
{
FirstName = "John",
LastName = "Doe",
Email = "john@example.com"
};
// Add dynamic properties
contact.SetDynamicProperty("Department", "Engineering");
contact.SetDynamicProperty("EmployeeNumber", 12345);
contact.SetDynamicProperty("StartDate", DateTime.Today);
contact.SetDynamicProperty("IsManager", true);
// Create the entity with dynamic properties
var created = await client.CreateAsync("Contacts", contact);contact.SetDynamicProperty("CustomAddress", new Address
{
Street = "123 Main St",
City = "Seattle",
PostalCode = "98101"
});// Dynamic properties can be updated like any other
await client.UpdateAsync<Contact>(
"Contacts",
1,
new
{
Department = "Marketing", // Update dynamic property
EmployeeNumber = 54321
}
);var contact = await client.GetByKeyAsync<Contact, int>(1);
if (contact.HasDynamicProperty("ObsoleteField"))
{
bool removed = contact.RemoveDynamicProperty("ObsoleteField");
Console.WriteLine($"Removed: {removed}");
}// Returns default if property doesn't exist
int count = contact.GetDynamicInt("ViewCount") ?? 0;
bool isActive = contact.GetDynamicBool("IsActive") ?? false;
string? category = contact.GetDynamicString("Category") ?? "Uncategorized";if (contact.TryGetDynamicProperty<decimal>("CustomPrice", out var price))
{
Console.WriteLine($"Price: {price:C}");
}
else
{
Console.WriteLine("No custom price set");
}var rating = contact.GetDynamicInt("Rating");
if (rating.HasValue)
{
Console.WriteLine($"Rating: {rating.Value}");
}
else
{
Console.WriteLine("Not rated");
}public class Product : ODataOpenType
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public int? CategoryId { get; set; }
}
public class CustomDimensions
{
public double? Height { get; set; }
public double? Width { get; set; }
public double? Depth { get; set; }
public string? Unit { get; set; }
}// Create product with custom fields
var product = new Product
{
Name = "Custom Widget",
Price = 29.99m
};
// Add various dynamic properties
product.SetDynamicProperty("SKU", "WDGT-001");
product.SetDynamicProperty("InStock", true);
product.SetDynamicProperty("StockCount", 150);
product.SetDynamicProperty("LastRestocked", DateTime.UtcNow);
product.SetDynamicProperty("Dimensions", new CustomDimensions
{
Height = 10.5,
Width = 5.0,
Depth = 3.0,
Unit = "cm"
});
// Create
var created = await client.CreateAsync("Products", product);
// Read back
var fetched = await client.GetByKeyAsync<Product, int>(created.Id);
Console.WriteLine($"Product: {fetched.Name}");
Console.WriteLine($"SKU: {fetched.GetDynamicString("SKU")}");
Console.WriteLine($"In Stock: {fetched.GetDynamicBool("InStock")}");
Console.WriteLine($"Stock Count: {fetched.GetDynamicInt("StockCount")}");
Console.WriteLine($"Last Restocked: {fetched.GetDynamicDateTime("LastRestocked")}");
var dimensions = fetched.GetDynamicProperty<CustomDimensions>("Dimensions");
if (dimensions != null)
{
Console.WriteLine($"Size: {dimensions.Height}x{dimensions.Width}x{dimensions.Depth} {dimensions.Unit}");
}
// List all custom properties
Console.WriteLine("\nAll dynamic properties:");
foreach (var propName in fetched.GetDynamicPropertyNames())
{
Console.WriteLine($" - {propName}");
}// Filter by dynamic property (if server supports it)
var query = client.For<Product>("Products")
.Filter("SKU eq 'WDGT-001'");
var products = await client.GetAsync(query);Dynamic properties are serialized alongside declared properties:
{
"Id": 1,
"Name": "Custom Widget",
"Price": 29.99,
"SKU": "WDGT-001",
"InStock": true,
"StockCount": 150,
"LastRestocked": "2024-01-15T10:30:00Z",
"Dimensions": {
"Height": 10.5,
"Width": 5.0,
"Depth": 3.0,
"Unit": "cm"
}
}- Use declared properties when possible - Only use dynamic properties for truly variable data
- Document expected dynamic properties - Even if not in schema, document commonly used fields
- Handle missing properties gracefully - Always check for existence or use null-coalescing
- Use consistent types - Don't store same property as different types across entities
- Consider validation - Validate dynamic property values in your application layer