Skip to content

Latest commit

 

History

History
315 lines (247 loc) · 7.52 KB

File metadata and controls

315 lines (247 loc) · 7.52 KB

Open Types (Dynamic Properties)

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.

Table of Contents

Overview

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

Defining Open Type Entities

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
}

Reading Dynamic Properties

Check If Property Exists

var contact = await client.GetByKeyAsync<Contact, int>(1);

if (contact.HasDynamicProperty("CustomField1"))
{
    Console.WriteLine("CustomField1 exists!");
}

Get String Property

var contact = await client.GetByKeyAsync<Contact, int>(1);

var customValue = contact.GetDynamicString("CustomField1");
Console.WriteLine($"Custom field: {customValue}");

Get Typed Properties

// 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");

Get Complex Type Property

// 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}");
}

Get All Dynamic Property Names

var contact = await client.GetByKeyAsync<Contact, int>(1);

foreach (var propertyName in contact.GetDynamicPropertyNames())
{
    Console.WriteLine($"Dynamic property: {propertyName}");
}

Writing Dynamic Properties

Set Dynamic Property

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);

Set Complex Type Property

contact.SetDynamicProperty("CustomAddress", new Address
{
    Street = "123 Main St",
    City = "Seattle",
    PostalCode = "98101"
});

Update Dynamic Properties

// Dynamic properties can be updated like any other
await client.UpdateAsync<Contact>(
    "Contacts", 
    1, 
    new 
    { 
        Department = "Marketing",  // Update dynamic property
        EmployeeNumber = 54321
    }
);

Remove Dynamic Property

var contact = await client.GetByKeyAsync<Contact, int>(1);

if (contact.HasDynamicProperty("ObsoleteField"))
{
    bool removed = contact.RemoveDynamicProperty("ObsoleteField");
    Console.WriteLine($"Removed: {removed}");
}

Type Conversions

Get with Default Value

// 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";

Try Get Pattern

if (contact.TryGetDynamicProperty<decimal>("CustomPrice", out var price))
{
    Console.WriteLine($"Price: {price:C}");
}
else
{
    Console.WriteLine("No custom price set");
}

Handle Missing Properties

var rating = contact.GetDynamicInt("Rating");
if (rating.HasValue)
{
    Console.WriteLine($"Rating: {rating.Value}");
}
else
{
    Console.WriteLine("Not rated");
}

Complete Example

Entity Model

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; }
}

Working with Dynamic Properties

// 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}");
}

Query with Dynamic Properties

// 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);

Serialization

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"
    }
}

Best Practices

  1. Use declared properties when possible - Only use dynamic properties for truly variable data
  2. Document expected dynamic properties - Even if not in schema, document commonly used fields
  3. Handle missing properties gracefully - Always check for existence or use null-coalescing
  4. Use consistent types - Don't store same property as different types across entities
  5. Consider validation - Validate dynamic property values in your application layer