universe

Universe

A simpler way of querying a CosmosDb Namespace

Installation

dotnet add package UniverseQuery

How-to:

  1. Your models / cosmos entities should inherit from the base class ```csharp public class MyCosmosEntity : CosmicEntity { [PartitionKey] public string FirstName { get; set; }

public string LastName { get; set; } }

This will allow you to use the `PartitionKey` attribute to specify the partition key for your Cosmos DB documents. You can also use multiple partition keys by specifying the order in the attribute, e.g., `[PartitionKey(1)]`, `[PartitionKey(2)]`, etc.

2. Create a repository like so:
```csharp
public class MyRepository : Galaxy<MyModel>
{
    public MyRepository(CosmosClient client, string database, string container, IReadOnlyList<string> partitionKey) : base(client, database, container, partitionKey)
    {
    }
}

// If you want to see debug information such as the full Query text executed, use the format below:
public class MyRepository : Galaxy<MyModel>
{
    public MyRepository(CosmosClient client, string database, string container, IReadOnlyList<string> partitionKey) : base(client, database, container, partitionKey, true)
    {
    }
}
  1. In your Startup.cs / Main method / Program.cs, configure the CosmosClient like so:
    _ = services.AddScoped(_ => new CosmosClient(
     System.Environment.GetEnvironmentVariable("CosmosDbUri"),
     System.Environment.GetEnvironmentVariable("CosmosDbPrimaryKey"),
     clientOptions: new CosmosClientOptions()
     {
         Serializer = new UniverseSerializer(), // This is from Universe.Builder.Options
         AllowBulkExecution = true // This will tell the underlying code to allow async bulk operations
     }
    ));
    

Below are the default options for the UniverseSerializer:

new JsonSerializerOptions()
{
    PropertyNamingPolicy = null, // To leave the property names as they are in the model
    PropertyNameCaseInsensitive = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
    IgnoreReadOnlyFields = true,
    IgnoreReadOnlyProperties = true
}
  1. In your Startup.cs / Main method / Program.cs, configure your CosmosDb repository like so:
    _ = services.AddScoped<IGalaxy<MyModel>, MyRepository>(service => new MyRepository(
     client: service.GetRequiredService<CosmosClient>(),
     database: "database-name",
     container: "container-name",
     partitionKey: typeof(MyModel).BuildPartitionKey()
    ));
    
  2. Inject your IGalaxy<MyModel> dependency into your classes and enjoy a simpler way to query CosmosDb

Understanding the Gravity Object

The Gravity object is returned by all operations and contains valuable information:

(Gravity gravity, MyModel model) = await galaxy.Get("document-id", "partition-key-value");

// Request Units consumed by the operation
double requestUnits = gravity.RU;

// Continuation token for pagination (only populated in Paged queries)
string continuationToken = gravity.ContinuationToken;

// Query information (only available when debug mode is enabled)
if (gravity.Query.HasValue)
{
    string queryText = gravity.Query.Value.Text;
    IEnumerable<(string, object)> parameters = gravity.Query.Value.Parameters;
    
    Console.WriteLine($"Query: {queryText}");
    foreach ((string name, object value) in parameters)
    {
        Console.WriteLine($"Parameter: {name} = {value}");
    }
}

Examples

This section provides examples of how to use the Galaxy repository for basic and advanced operations with Cosmos DB. Here.

Basic Operations

Simple Query Operations

// Get a single document by id and partition key
(Gravity gravity, MyModel model) = await galaxy.Get("document-id", "partition-key-value");

// Basic query with a single filter condition
(Gravity gravity, MyModel model) = await galaxy.Get(
    clusters: new List<Cluster>() 
    {
        new Cluster(Catalysts: new List<Catalyst>
        {
            new Catalyst(nameof(MyModel.PropertyName), "value")
        })
    }
);
// Get a single document by id and multiple partition keys
(Gravity gravity, MyModel model) = await galaxy.Get("document-id", "partition-key-value1", "partition-key-value2");

Creating Documents

// Create a single document
MyModel model = new MyModel 
{ 
    PropertyName = "value",
    // Set other properties
};
(Gravity gravity, string id) = await galaxy.Create(model);

// Bulk create multiple documents
List<MyModel> models = new List<MyModel>
{
    new MyModel { /* properties */ },
    new MyModel { /* properties */ }
};
Gravity gravity = await galaxy.Create(models);

Updating Documents

// Update a single document
model.PropertyName = "new value";
(Gravity gravity, MyModel updatedModel) = await galaxy.Modify(model);

// Bulk update multiple documents
foreach (MyModel item in models)
{
    item.PropertyName = "new value";
}
Gravity gravity = await galaxy.Modify(models);

Deleting Documents

// Delete a document
Gravity gravity = await galaxy.Remove("document-id", "partition-key-value");
// Delete a document by id and multiple partition keys
Gravity gravity = await galaxy.Remove("document-id", "partition-key-value1", "partition-key-value2");

Advanced Query Examples

Complex Queries with Multiple Conditions

// Query with multiple conditions in a single cluster
(Gravity gravity, IList<MyModel> results) = await galaxy.List(
    clusters: new List<Cluster>()
    {
        new Cluster(Catalysts: new List<Catalyst>
        {
            new Catalyst(nameof(MyModel.PropertyName), "value"),
            new Catalyst(nameof(MyModel.NumberProperty), 123, Where: Q.Where.And)
        })
    }
);

// Query with multiple clusters (combining conditions with AND/OR)
(Gravity gravity, IList<MyModel> results) = await galaxy.List(
    clusters: new List<Cluster>()
    {
        new Cluster(Catalysts: new List<Catalyst>
        {
            new Catalyst(nameof(MyModel.PropertyName), "value"),
            new Catalyst(nameof(MyModel.AnotherProperty), 123, Where: Q.Where.Or)
        }, Where: Q.Where.And),
        new Cluster(Catalysts: new List<Catalyst>
        {
            new Catalyst(nameof(MyModel.Status), "Active")
        })
    }
);

Special Operators

// Using In operator for array properties
(Gravity gravity, IList<MyModel> results) = await galaxy.List(
    clusters: new List<Cluster>()
    {
        new Cluster(Catalysts: new List<Catalyst>
        {
            new Catalyst(nameof(MyModel.Tags), "tag1", Operator: Q.Operator.In)
        })
    }
);

// Check if a property is defined
(Gravity gravity, IList<MyModel> results) = await galaxy.List(
    clusters: new List<Cluster>()
    {
        new Cluster(Catalysts: new List<Catalyst>
        {
            new Catalyst(nameof(MyModel.OptionalProperty), Operator: Q.Operator.Defined)
        })
    }
);

// Comparison operators
(Gravity gravity, IList<MyModel> results) = await galaxy.List(
    clusters: new List<Cluster>()
    {
        new Cluster(Catalysts: new List<Catalyst>
        {
            new Catalyst(nameof(MyModel.NumberProperty), 100, Operator: Q.Operator.Gt)
        })
    }
);

Sorting and Column Selection

// Query with sorting
(Gravity gravity, IList<MyModel> results) = await galaxy.List(
    clusters: new List<Cluster>() { /* query conditions */ },
    sorting: new List<Sorting.Option>
    {
        new Sorting.Option(nameof(MyModel.PropertyName), Sorting.Direction.DESC)
    }
);

// Query with column selection
(Gravity gravity, IList<MyModel> results) = await galaxy.List(
    clusters: new List<Cluster>() { /* query conditions */ },
    columnOptions: new ColumnOptions(
        Names: new List<string>
        {
            nameof(MyModel.id),
            nameof(MyModel.PropertyName),
            nameof(MyModel.AnotherProperty)
        }
    )
);

// Using TOP to limit results
(Gravity gravity, IList<MyModel> results) = await galaxy.List(
    clusters: new List<Cluster>() { /* query conditions */ },
    columnOptions: new ColumnOptions(
        Names: new List<string>
        {
            nameof(MyModel.id),
            nameof(MyModel.PropertyName)
        },
        Top: 10
    )
);

// Using DISTINCT
(Gravity gravity, IList<MyModel> results) = await galaxy.List(
    clusters: new List<Cluster>() { /* query conditions */ },
    columnOptions: new ColumnOptions(
        Names: new List<string>
        {
            nameof(MyModel.PropertyName)
        },
        IsDistinct: true
    )
);

See example 1, example 3.

Pagination

// First page
(Gravity gravity, IList<MyModel> items) = await galaxy.Paged(
    page: new Q.Page(25), // 25 items per page
    clusters: new List<Cluster>() { /* query conditions */ }
);

// Access continuation token from the gravity object
string continuationToken = gravity.ContinuationToken;

// Next page using continuation token
(Gravity nextGravity, IList<MyModel> nextItems) = await galaxy.Paged(
    page: new Q.Page(25, continuationToken),
    clusters: new List<Cluster>() { /* same query conditions */ }
);

See example 1.

Aggregation and Group By Queries

// Group by a property
(Gravity gravity, IList<MyModel> results) = await galaxy.List(
    clusters: new List<Cluster>() { /* query conditions */ },
    group: new List<string> { nameof(MyModel.Category) }
);

// Using aggregation functions with ColumnOptions.Aggregates
(Gravity gravity, IList<MyModel> results) = await galaxy.List(
    clusters: new List<Cluster>() { /* query conditions */ },
    columnOptions: new ColumnOptions(
        Names: new List<string> { nameof(MyModel.Category) },
        Aggregates: [
            new AggregationOption(nameof(MyModel.Price), Q.Aggregate.Sum),
            new AggregationOption(nameof(MyModel.Quantity), Q.Aggregate.Count)
        ]
    )
);

// Multiple aggregation functions in one query
(Gravity gravity, IList<MyModel> results) = await galaxy.List(
    clusters: new List<Cluster>() { /* query conditions */ },
    columnOptions: new ColumnOptions(
        Names: new List<string> { nameof(MyModel.Category) },
        Aggregates: [
            new AggregationOption(nameof(MyModel.Price), Q.Aggregate.Sum),
            new AggregationOption(nameof(MyModel.Price), Q.Aggregate.Avg),
            new AggregationOption(nameof(MyModel.Quantity), Q.Aggregate.Max)
        ]
    )
);

The Aggregates parameter in ColumnOptions takes an array of AggregationOption structs, each specifying a column name and an aggregate function to apply. It supports the following aggregate functions:

When using aggregates, the query will automatically be grouped by the columns specified in the Names parameter. The output column names will be suffixed with the aggregate function name (e.g., Price_Sum, Price_Avg, Quantity_Max).

See example 2, example 7, example 8.

The Universe library supports vector similarity search through the Q.Operator.VectorDistance operator, which leverages Azure Cosmos DB’s built-in vector search capabilities.

See the VECTORDISTANCE_USAGE.md

The Universe library provides a simple way to perform full-text search queries using the Q.Operator.FT* operators. This allows you to search for documents containing specific text in designated fields.

See the FULLTEXT_USAGE.md

Stored Procedures

You can manage and execute Cosmos DB stored procedures using the IGalaxyProcedure interface. Inject your repository as IGalaxyProcedure and use its methods for full stored procedure lifecycle management and execution.

IGalaxyProcedure galaxyProcedure = ...; // Injected or resolved from DI

// Execute a stored procedure and get a result of type T
(Gravity gravity, MyModel result) = await galaxyProcedure.ExecSProc<MyModel>(
    procedureName: "myStoredProcedure",
    partitionKey: "partition-key-value",
    parameters: new object[] { /* procedure parameters */ }
);

// Create a new stored procedure
Gravity createResult = await galaxyProcedure.CreateSProc(
    procedureName: "myStoredProcedure",
    body: "function (...) { /* JS code */ }"
);

// Read a stored procedure's body
(Gravity readGravity, string body) = await galaxyProcedure.ReadSProc("myStoredProcedure");

// Replace an existing stored procedure
Gravity replaceResult = await galaxyProcedure.ReplaceSProc(
    procedureName: "myStoredProcedure",
    newBody: "function (...) { /* new JS code */ }"
);

// Delete a stored procedure
Gravity deleteResult = await galaxyProcedure.DeleteSProc("myStoredProcedure");

// List all stored procedure names
(Gravity listGravity, IList<string> names) = await galaxyProcedure.ListSProcs();

The Gravity object provides RU and diagnostic information for each operation.

Error Handling

try
{
    (Gravity gravity, MyModel model) = await galaxy.Get("non-existent-id", "partition-key");
}
catch (UniverseException ex)
{
    // Universe-specific exceptions
    Console.WriteLine($"Universe error: {ex.Message}");
}
catch (CosmosException ex)
{
    // Cosmos DB specific exceptions
    Console.WriteLine($"Cosmos error: {ex.Message}, Status: {ex.StatusCode}");
}
catch (Exception ex)
{
    // Other errors
    Console.WriteLine($"Error: {ex.Message}");
}

Performance Considerations