The Weaviate C# client provides strongly-typed accessor methods for working with aggregation results. These accessors eliminate the need to manually cast from the Properties dictionary, providing compile-time type safety and improved developer experience.
- Type Safety: No more manual casting from
objectto specific aggregation types - Null Safety: Methods return
nullwhen property doesn't exist or is wrong type - TryGet Pattern: Familiar .NET pattern for safe access with output parameters
- Pattern Matching: Lambda-based matching for flexible handling of different aggregation types
- IDE Support: Full IntelliSense for all accessor methods
var result = await collection.Aggregate.OverAll(
returnMetrics: [Metrics.ForProperty("price").Number()]
);
// Manual casting - error-prone
var priceAgg = result.Properties["price"] as Aggregate.Number;
if (priceAgg != null)
{
Console.WriteLine($"Average price: {priceAgg.Mean}");
}var result = await collection.Aggregate.OverAll(
returnMetrics: [Metrics.ForProperty("price").Number()]
);
// Option 1: Direct accessor
var priceAgg = result.Number("price");
if (priceAgg != null)
{
Console.WriteLine($"Average price: {priceAgg.Mean}");
}
// Option 2: TryGet pattern
if (result.TryGetNumber("price", out var price))
{
Console.WriteLine($"Average price: {price.Mean}");
}
// Option 3: Pattern matching
result.Match("price",
number: n => Console.WriteLine($"Average price: {n.Mean}")
);Simple methods that return the typed aggregation or null.
| Method | Returns | Use Case |
|---|---|---|
Text(propertyName) |
Aggregate.Text? |
Text/string properties |
Integer(propertyName) |
Aggregate.Integer? |
Int/long properties |
Number(propertyName) |
Aggregate.Number? |
Float/double properties |
Boolean(propertyName) |
Aggregate.Boolean? |
Boolean properties |
Date(propertyName) |
Aggregate.Date? |
DateTime properties |
var result = await collection.Aggregate.OverAll(
returnMetrics: [
Metrics.ForProperty("title").Text(),
Metrics.ForProperty("price").Number(),
Metrics.ForProperty("quantity").Integer(),
Metrics.ForProperty("inStock").Boolean(),
Metrics.ForProperty("createdAt").Date()
]
);
// Access text aggregation
var titleAgg = result.Text("title");
if (titleAgg != null)
{
Console.WriteLine($"Title count: {titleAgg.Count}");
foreach (var occurrence in titleAgg.TopOccurrences)
{
Console.WriteLine($" {occurrence.Value}: {occurrence.Count}");
}
}
// Access numeric aggregation
var priceAgg = result.Number("price");
if (priceAgg != null)
{
Console.WriteLine($"Price - Min: {priceAgg.Minimum}, Max: {priceAgg.Maximum}, Mean: {priceAgg.Mean}");
}
// Access integer aggregation
var quantityAgg = result.Integer("quantity");
if (quantityAgg != null)
{
Console.WriteLine($"Total quantity: {quantityAgg.Sum}");
}
// Access boolean aggregation
var inStockAgg = result.Boolean("inStock");
if (inStockAgg != null)
{
Console.WriteLine($"In stock: {inStockAgg.PercentageTrue:P0}");
}
// Access date aggregation
var dateAgg = result.Date("createdAt");
if (dateAgg != null)
{
Console.WriteLine($"Date range: {dateAgg.Minimum} to {dateAgg.Maximum}");
}Methods following the .NET TryGet pattern for safe access with output parameters.
| Method | Output Parameter | Returns |
|---|---|---|
TryGetText(propertyName, out text) |
Aggregate.Text |
bool |
TryGetInteger(propertyName, out integer) |
Aggregate.Integer |
bool |
TryGetNumber(propertyName, out number) |
Aggregate.Number |
bool |
TryGetBoolean(propertyName, out boolean) |
Aggregate.Boolean |
bool |
TryGetDate(propertyName, out date) |
Aggregate.Date |
bool |
TryGet<T>(propertyName, out aggregation) |
T |
bool |
// Specific TryGet methods
if (result.TryGetNumber("price", out var price))
{
Console.WriteLine($"Average price: {price.Mean}");
Console.WriteLine($"Price range: {price.Minimum} - {price.Maximum}");
}
else
{
Console.WriteLine("Price aggregation not available");
}
if (result.TryGetText("category", out var category))
{
Console.WriteLine($"Categories found: {category.Count}");
}
// Generic TryGet
if (result.TryGet<Aggregate.Boolean>("inStock", out var stockAgg))
{
Console.WriteLine($"In stock: {stockAgg.TotalTrue}, Out of stock: {stockAgg.TotalFalse}");
}Lambda-based methods for flexible handling of aggregation types.
Execute an action or function on a specific type:
// Action variant - execute if matches
result.Property<Aggregate.Number>("price", price =>
{
Console.WriteLine($"Min: {price.Minimum}");
Console.WriteLine($"Max: {price.Maximum}");
Console.WriteLine($"Mean: {price.Mean}");
});
// Func variant - return a value
var summary = result.Property<Aggregate.Number, string>("price",
price => $"Price range: ${price.Minimum} - ${price.Maximum}"
);Handle multiple types with a single call:
// Action variant - execute the matching handler
result.Match("field",
text: t => Console.WriteLine($"Text: {t.Count} occurrences"),
integer: i => Console.WriteLine($"Integer: sum={i.Sum}"),
number: n => Console.WriteLine($"Number: mean={n.Mean}"),
boolean: b => Console.WriteLine($"Boolean: {b.PercentageTrue:P0} true"),
date: d => Console.WriteLine($"Date range: {d.Minimum} to {d.Maximum}")
);
// Func variant - return a value based on type
var description = result.Match<string>("field",
text: t => $"Text with {t.TopOccurrences.Count} unique values",
integer: i => $"Integer ranging from {i.Minimum} to {i.Maximum}",
number: n => $"Number with mean {n.Mean:F2}",
boolean: b => $"Boolean: {b.PercentageTrue:P0} true",
date: d => $"Dates from {d.Minimum:d} to {d.Maximum:d}"
);
Console.WriteLine(description ?? "Unknown type");Iterate through all properties with type-safe handling:
foreach (var (name, _) in result.Properties)
{
var description = result.Match<string>(name,
text: t => $"{name}: {t.Count} items, top: {t.TopOccurrences.FirstOrDefault()?.Value}",
integer: i => $"{name}: range [{i.Minimum}, {i.Maximum}], sum: {i.Sum}",
number: n => $"{name}: range [{n.Minimum:F2}, {n.Maximum:F2}], mean: {n.Mean:F2}",
boolean: b => $"{name}: {b.TotalTrue} true, {b.TotalFalse} false",
date: d => $"{name}: {d.Minimum:d} to {d.Maximum:d}"
);
Console.WriteLine(description ?? $"{name}: unsupported type");
}All accessor methods are also available on AggregateGroupByResult.Group:
var result = await collection.Aggregate.OverAll(
groupBy: "category",
returnMetrics: [Metrics.ForProperty("price").Number()]
);
foreach (var group in result.Groups)
{
Console.WriteLine($"Category: {group.GroupedBy.Value}");
Console.WriteLine($" Count: {group.TotalCount}");
// Direct accessor
var priceAgg = group.Number("price");
if (priceAgg != null)
{
Console.WriteLine($" Avg Price: {priceAgg.Mean:C}");
}
// Or use TryGet
if (group.TryGetNumber("price", out var price))
{
Console.WriteLine($" Price Range: {price.Minimum:C} - {price.Maximum:C}");
}
// Or use Match
group.Match("price",
number: n => Console.WriteLine($" Median Price: {n.Median:C}")
);
}For text/string properties:
| Property | Type | Description |
|---|---|---|
Count |
long? |
Number of values |
TopOccurrences |
List<TopOccurrence<string>> |
Most frequent values |
For integer/long properties:
| Property | Type | Description |
|---|---|---|
Count |
long? |
Number of values |
Minimum |
long? |
Minimum value |
Maximum |
long? |
Maximum value |
Mean |
double? |
Average value |
Median |
double? |
Median value |
Mode |
long? |
Most common value |
Sum |
long? |
Sum of all values |
For float/double properties:
| Property | Type | Description |
|---|---|---|
Count |
long? |
Number of values |
Minimum |
double? |
Minimum value |
Maximum |
double? |
Maximum value |
Mean |
double? |
Average value |
Median |
double? |
Median value |
Mode |
double? |
Most common value |
Sum |
double? |
Sum of all values |
For boolean properties:
| Property | Type | Description |
|---|---|---|
Count |
long? |
Number of values |
TotalTrue |
long |
Count of true values |
TotalFalse |
long |
Count of false values |
PercentageTrue |
double |
Percentage of true values (0-1) |
PercentageFalse |
double |
Percentage of false values (0-1) |
For date/datetime properties:
| Property | Type | Description |
|---|---|---|
Count |
long? |
Number of values |
Minimum |
DateTime? |
Earliest date |
Maximum |
DateTime? |
Latest date |
Median |
DateTime? |
Median date |
Mode |
DateTime? |
Most common date |
// Use direct accessor when you just need the value
var price = result.Number("price");
// Use TryGet when you need to handle missing properties
if (result.TryGetNumber("price", out var p))
{
// Use p
}
// Use Match when handling multiple possible types
result.Match("unknownField",
text: t => /* handle text */,
number: n => /* handle number */
);// Direct accessor returns null if not found or wrong type
var price = result.Number("price");
var average = price?.Mean ?? 0;
// Or use null-conditional operator
Console.WriteLine($"Mean: {result.Number("price")?.Mean}");// When you don't know the type at compile time
foreach (var propertyName in propertyNames)
{
result.Match(propertyName,
text: t => ProcessText(t),
integer: i => ProcessInteger(i),
number: n => ProcessNumber(n),
boolean: b => ProcessBoolean(b),
date: d => ProcessDate(d)
);
}// Direct accessors
Aggregate.Text? Text(string propertyName)
Aggregate.Integer? Integer(string propertyName)
Aggregate.Number? Number(string propertyName)
Aggregate.Boolean? Boolean(string propertyName)
Aggregate.Date? Date(string propertyName)
// TryGet methods
bool TryGetText(string propertyName, out Aggregate.Text text)
bool TryGetInteger(string propertyName, out Aggregate.Integer integer)
bool TryGetNumber(string propertyName, out Aggregate.Number number)
bool TryGetBoolean(string propertyName, out Aggregate.Boolean boolean)
bool TryGetDate(string propertyName, out Aggregate.Date date)
bool TryGet<T>(string propertyName, out T aggregation) where T : Aggregate.Property
// Pattern matching
bool Property<T>(string propertyName, Action<T> action) where T : Aggregate.Property
TResult? Property<T, TResult>(string propertyName, Func<T, TResult> func) where T : Aggregate.Property
bool Match(string propertyName,
Action<Aggregate.Text>? text = null,
Action<Aggregate.Integer>? integer = null,
Action<Aggregate.Number>? number = null,
Action<Aggregate.Boolean>? boolean = null,
Action<Aggregate.Date>? date = null)
TResult? Match<TResult>(string propertyName,
Func<Aggregate.Text, TResult>? text = null,
Func<Aggregate.Integer, TResult>? integer = null,
Func<Aggregate.Number, TResult>? number = null,
Func<Aggregate.Boolean, TResult>? boolean = null,
Func<Aggregate.Date, TResult>? date = null)All the same methods are available on AggregateGroupByResult.Group.
In addition to the accessor methods described above, you can use the ToTyped<T>() extension method to automatically map aggregation results to a strongly-typed object. This is particularly useful when you want to work with aggregation data in a structured way.
- Execute an aggregation query using the
AggregateClient - Call
ToTyped<T>()on the result to map it to your custom type - Access the mapped properties directly
The mapper supports two approaches for property definitions:
- Full Aggregate Types: Use
Aggregate.Text,Aggregate.Integer, etc. as property types to get the complete aggregation data - Suffix-Based Extraction: Use primitive types with specific suffixes to extract individual values (e.g.,
PriceSum,RatingMean,TitleCount)
When you want access to all aggregation properties, use the full Aggregate.* types:
public class ArticleAggregations
{
public Aggregate.Text? Title { get; set; }
public Aggregate.Integer? WordCount { get; set; }
public Aggregate.Number? Rating { get; set; }
public Aggregate.Boolean? IsPublished { get; set; }
public Aggregate.Date? PublishedDate { get; set; }
}
// Execute aggregation and convert to typed result
var collection = weaviate.UseCollection("Articles");
var result = await collection.Aggregate.OverAll(
returnMetrics: [
Metrics.ForProperty("title").Text(),
Metrics.ForProperty("wordCount").Integer(),
Metrics.ForProperty("rating").Number(),
Metrics.ForProperty("isPublished").Boolean(),
Metrics.ForProperty("publishedDate").Date()
]
);
// Convert to typed result
var typed = result.ToTyped<ArticleAggregations>();
// Access typed properties directly
Console.WriteLine($"Title count: {typed.Properties.Title?.Count}");
Console.WriteLine($"Average rating: {typed.Properties.Rating?.Mean}");
Console.WriteLine($"Word count range: {typed.Properties.WordCount?.Minimum} - {typed.Properties.WordCount?.Maximum}");
Console.WriteLine($"Published: {typed.Properties.IsPublished?.PercentageTrue:P0}");Use property name suffixes to explicitly indicate which value to extract:
public class ArticleSummary
{
// Field "title" + suffix "Count" = TitleCount
public long? TitleCount { get; set; }
// Field "rating" + suffix "Mean" = RatingMean
public double? RatingMean { get; set; }
// Field "wordCount" + suffix "Sum" = WordCountSum
public long? WordCountSum { get; set; }
// Field "isPublished" + suffix "PercentageTrue" = IsPublishedPercentageTrue
public double? IsPublishedPercentageTrue { get; set; }
// Field "publishedDate" + suffix "Minimum" = PublishedDateMinimum
public DateTime? PublishedDateMinimum { get; set; }
}
var result = await collection.Aggregate.OverAll(
returnMetrics: [
Metrics.ForProperty("title").Text(),
Metrics.ForProperty("rating").Number(),
Metrics.ForProperty("wordCount").Integer(),
Metrics.ForProperty("isPublished").Boolean(),
Metrics.ForProperty("publishedDate").Date()
]
);
// Convert to typed result with suffix-based extraction
var typed = result.ToTyped<ArticleSummary>();
// Access extracted values directly
Console.WriteLine($"Articles with titles: {typed.Properties.TitleCount}");
Console.WriteLine($"Average rating: {typed.Properties.RatingMean}");
Console.WriteLine($"Total words: {typed.Properties.WordCountSum}");
Console.WriteLine($"Percentage published: {typed.Properties.IsPublishedPercentageTrue:P0}");
Console.WriteLine($"Earliest: {typed.Properties.PublishedDateMinimum}");| Suffix | Applicable Types | Expected Property Type | Description |
|---|---|---|---|
Count |
All | long, int |
Number of values |
Sum |
Integer, Number | long, int, double, float |
Sum of all values |
Mean, Average |
Integer, Number | double, float |
Average value |
Min, Minimum |
Integer, Number, Date | long, int, double, DateTime |
Minimum value |
Max, Maximum |
Integer, Number, Date | long, int, double, DateTime |
Maximum value |
Median |
Integer, Number, Date | double, DateTime |
Median value |
Mode |
Integer, Number, Date | long, int, double, DateTime |
Most common value |
TotalTrue |
Boolean | long, int |
Count of true values |
TotalFalse |
Boolean | long, int |
Count of false values |
PercentageTrue |
Boolean | double, float |
Percentage of true (0.0-1.0) |
PercentageFalse |
Boolean | double, float |
Percentage of false (0.0-1.0) |
TopOccurrence |
Text | string |
Most frequent value |
TopOccurrences |
Text | List<Aggregate.TopOccurrence<string>> |
All top occurrences |
You can mix full types and suffix-based extraction in the same class:
public class ProductStats
{
// Full type for complete access
public Aggregate.Number? Price { get; set; }
// Extract specific values with suffixes
public double? PriceMean { get; set; }
public double? PriceSum { get; set; }
public long? QuantitySum { get; set; }
public long? CategoryCount { get; set; }
}Instead of manually specifying the returnMetrics parameter, you can use MetricsExtractor.FromType<T>() to automatically generate the appropriate metrics from your type definition:
using Weaviate.Client.Models.Typed;
public class ProductStats
{
public double? PriceMean { get; set; }
public double? PriceMin { get; set; }
public double? PriceMax { get; set; }
public long? QuantitySum { get; set; }
public Aggregate.Text? Category { get; set; }
}
// Automatically extract metrics from the type
var metrics = MetricsExtractor.FromType<ProductStats>();
// Use the extracted metrics in the query
var result = await collection.Aggregate.OverAll(returnMetrics: metrics);
// Convert to typed result
var typed = result.ToTyped<ProductStats>();The MetricsExtractor analyzes your type and:
- For Aggregate. types* (e.g.,
Aggregate.Text,Aggregate.Number), it enables all metric flags for that property - For suffix-based properties (e.g.,
PriceMean,QuantitySum), it enables only the specific metric flags needed
This approach ensures the query only requests the metrics you actually need, and the type definition serves as the single source of truth for both the query and the result mapping.
When you have multiple properties with different suffixes for the same field, the metrics are combined:
public class PriceAnalysis
{
public double? PriceMean { get; set; }
public double? PriceMin { get; set; }
public double? PriceMax { get; set; }
public long? PriceCount { get; set; }
}
// This extracts a single Number metric for "price" with Mean, Minimum, Maximum, and Count enabled
var metrics = MetricsExtractor.FromType<PriceAnalysis>();When using full Aggregate.* types, you can specify which metrics to query using type-specific attributes. This provides excellent IntelliSense guidance and reduces network traffic by querying only the metrics you need:
using Weaviate.Client.Models.Typed;
public class ProductStats
{
// Query only Min, Max, Mean (not Sum, Median, Mode, Count)
[NumberMetrics(Minimum = true, Maximum = true, Mean = true)]
public Aggregate.Number? Price { get; set; }
// Query all Integer metrics (no attribute)
public Aggregate.Integer? Quantity { get; set; }
// Query Count and TopOccurrences with minimum threshold
[TextMetrics(Count = true, TopOccurrences = true, MinOccurrences = 5)]
public Aggregate.Text? Category { get; set; }
// Query only specific Boolean metrics
[BooleanMetrics(TotalTrue = true, PercentageTrue = true)]
public Aggregate.Boolean? Featured { get; set; }
}
var metrics = MetricsExtractor.FromType<ProductStats>();
var result = await collection.Aggregate.OverAll(returnMetrics: metrics);
var typed = result.ToTyped<ProductStats>();Available attributes:
| Attribute | Applicable Type | Available Properties |
|---|---|---|
[TextMetrics] |
Aggregate.Text |
Count, TopOccurrences, MinOccurrences |
[IntegerMetrics] |
Aggregate.Integer |
Count, Sum, Mean, Minimum, Maximum, Median, Mode |
[NumberMetrics] |
Aggregate.Number |
Count, Sum, Mean, Minimum, Maximum, Median, Mode |
[BooleanMetrics] |
Aggregate.Boolean |
Count, TotalTrue, TotalFalse, PercentageTrue, PercentageFalse |
[DateMetrics] |
Aggregate.Date |
Count, Minimum, Maximum, Median, Mode |
Benefits:
- Excellent IntelliSense showing available metrics for each type
- Compile-time safety - C# compiler prevents mismatches
- Reduces network traffic by querying only needed metrics
- Clean, readable syntax with named boolean properties
- Properties without attributes query all metrics (backward compatible)
Example with IntelliSense:
When you type [NumberMetrics( in your IDE, IntelliSense will show exactly which properties are available (Minimum, Maximum, Mean, Sum, Count, Median, Mode), making it clear what metrics can be queried for Number types.
MinOccurrences parameter:
The TextMetrics attribute includes a special MinOccurrences parameter that sets the minimum occurrence threshold for top occurrences:
public class CategoryAnalysis
{
// Only show categories that appear at least 10 times
[TextMetrics(TopOccurrences = true, MinOccurrences = 10)]
public Aggregate.Text? Category { get; set; }
}Analyzer validation:
If you use the wrong attribute type (e.g., [NumberMetrics] on an Aggregate.Text property), you'll get a WEAVIATE007 warning at compile time pointing out the mismatch.
Typed mapping also works with grouped aggregations using ToTyped<T>():
public class CategoryStats
{
public Aggregate.Number? Rating { get; set; }
public double? RatingMean { get; set; }
public long? RatingCount { get; set; }
}
var result = await collection.Aggregate.OverAll(
groupBy: new Aggregate.GroupBy("category"),
returnMetrics: [Metrics.ForProperty("rating").Number()]
);
// Convert grouped result to typed
var typed = result.ToTyped<CategoryStats>();
foreach (var group in typed.Groups)
{
Console.WriteLine($"Category: {group.GroupedBy.Value}");
Console.WriteLine($" Count: {group.TotalCount}");
Console.WriteLine($" Avg Rating: {group.Properties.Rating?.Mean}");
// Or using suffix extraction:
Console.WriteLine($" Avg Rating: {group.Properties.RatingMean}");
}When grouping, you can aggregate multiple properties and access them all in a typed manner:
public class ProductCategoryAnalysis
{
// Price statistics
public Aggregate.Number? Price { get; set; }
public double? PriceMean { get; set; }
public double? PriceMin { get; set; }
public double? PriceMax { get; set; }
// Stock statistics
public Aggregate.Integer? StockQuantity { get; set; }
public long? StockQuantitySum { get; set; }
// Availability
public double? InStockPercentageTrue { get; set; }
}
var result = await collection.Aggregate.OverAll(
groupBy: new Aggregate.GroupBy("category"),
returnMetrics: [
Metrics.ForProperty("price").Number(),
Metrics.ForProperty("stockQuantity").Integer(),
Metrics.ForProperty("inStock").Boolean()
]
);
var typed = result.ToTyped<ProductCategoryAnalysis>();
// Generate a report for each category
foreach (var group in typed.Groups)
{
Console.WriteLine($"=== {group.GroupedBy.Value} ===");
Console.WriteLine($"Products in category: {group.TotalCount}");
Console.WriteLine($"Price range: ${group.Properties.PriceMin:F2} - ${group.Properties.PriceMax:F2}");
Console.WriteLine($"Average price: ${group.Properties.PriceMean:F2}");
Console.WriteLine($"Total stock: {group.Properties.StockQuantitySum}");
Console.WriteLine($"In stock: {group.Properties.InStockPercentageTrue:P0}");
Console.WriteLine();
}The typed groups work seamlessly with LINQ for analysis:
public class SalesRegionStats
{
public double? RevenueMean { get; set; }
public double? RevenueSum { get; set; }
public long? OrdersCount { get; set; }
}
var result = await collection.Aggregate.OverAll(
groupBy: new Aggregate.GroupBy("region"),
returnMetrics: [
Metrics.ForProperty("revenue").Number(),
Metrics.ForProperty("orders").Integer()
]
);
var typed = result.ToTyped<SalesRegionStats>();
// Find top performing regions
var topRegions = typed.Groups
.Where(g => g.Properties.RevenueSum.HasValue)
.OrderByDescending(g => g.Properties.RevenueSum)
.Take(5);
foreach (var region in topRegions)
{
Console.WriteLine($"{region.GroupedBy.Value}: ${region.Properties.RevenueSum:N0} total revenue");
}
// Calculate overall statistics
var totalRevenue = typed.Groups.Sum(g => g.Properties.RevenueSum ?? 0);
var avgRevenuePerRegion = typed.Groups.Average(g => g.Properties.RevenueMean ?? 0);
Console.WriteLine($"Total revenue across all regions: ${totalRevenue:N0}");
Console.WriteLine($"Average revenue per region: ${avgRevenuePerRegion:N0}");Access group metadata alongside typed properties:
public class AuthorStats
{
public long? ArticlesCount { get; set; }
public double? WordCountMean { get; set; }
public long? WordCountSum { get; set; }
public double? RatingMean { get; set; }
}
var result = await collection.Aggregate.OverAll(
groupBy: new Aggregate.GroupBy("author"),
returnMetrics: [
Metrics.ForProperty("title").Text(),
Metrics.ForProperty("wordCount").Integer(),
Metrics.ForProperty("rating").Number()
]
);
var typed = result.ToTyped<AuthorStats>();
// Build a dictionary for quick lookups
var authorLookup = typed.Groups.ToDictionary(
g => g.GroupedBy.Value?.ToString() ?? "Unknown",
g => g.Properties
);
// Access specific author's stats
if (authorLookup.TryGetValue("John Doe", out var johnStats))
{
Console.WriteLine($"John Doe has written {johnStats.ArticlesCount} articles");
Console.WriteLine($"Total words: {johnStats.WordCountSum:N0}");
Console.WriteLine($"Average rating: {johnStats.RatingMean:F1}/5");
}
// Find the most prolific author
var mostProlific = typed.Groups
.OrderByDescending(g => g.Properties.ArticlesCount ?? 0)
.FirstOrDefault();
if (mostProlific != null)
{
Console.WriteLine($"Most prolific author: {mostProlific.GroupedBy.Value} " +
$"with {mostProlific.Properties.ArticlesCount} articles");
}The typed result always preserves access to the underlying untyped result:
var result = await collection.Aggregate.OverAll(
returnMetrics: [Metrics.ForProperty("rating").Number()]
);
var typed = result.ToTyped<ArticleAggregations>();
// Access untyped result when needed
var untypedResult = typed.Untyped;
Console.WriteLine($"Total count: {untypedResult.TotalCount}");
// Use accessor methods on untyped result
if (untypedResult.TryGetNumber("rating", out var rating))
{
Console.WriteLine($"Rating median: {rating.Median}");
}Here's a complete example showing different ways to work with aggregate results:
// Define your aggregation result type
public class ProductStats
{
// Full aggregate types for complete access
public Aggregate.Number? Price { get; set; }
public Aggregate.Integer? Quantity { get; set; }
// Suffix-based extraction for specific values
public double? PriceMean { get; set; }
public double? PriceMin { get; set; }
public double? PriceMax { get; set; }
public long? QuantitySum { get; set; }
public long? CategoryCount { get; set; }
}
// Execute aggregation
var result = await collection.Aggregate.OverAll(
returnMetrics: [
Metrics.ForProperty("price").Number(),
Metrics.ForProperty("quantity").Integer(),
Metrics.ForProperty("category").Text()
]
);
// Option 1: Use accessor methods directly
var avgPrice = result.Number("price")?.Mean;
// Option 2: Use TryGet pattern
if (result.TryGetNumber("price", out var priceAgg))
{
Console.WriteLine($"Price range: {priceAgg.Minimum} - {priceAgg.Maximum}");
}
// Option 3: Convert to typed object
var stats = result.ToTyped<ProductStats>();
Console.WriteLine($"Average price: {stats.Properties.PriceMean}");
Console.WriteLine($"Price range: {stats.Properties.PriceMin} - {stats.Properties.PriceMax}");
Console.WriteLine($"Total quantity: {stats.Properties.QuantitySum}");
// Full aggregate access is also available
Console.WriteLine($"Price median: {stats.Properties.Price?.Median}");Build a product analytics dashboard with category breakdowns:
public class CategoryDashboard
{
public long? ProductCount { get; set; }
public double? PriceMean { get; set; }
public double? PriceMin { get; set; }
public double? PriceMax { get; set; }
public long? StockSum { get; set; }
public double? InStockPercentageTrue { get; set; }
public double? RatingMean { get; set; }
}
public async Task<Dictionary<string, CategoryDashboard>> GetCategoryDashboard(
CollectionClient collection)
{
var result = await collection.Aggregate.OverAll(
groupBy: new Aggregate.GroupBy("category"),
returnMetrics: [
Metrics.ForProperty("name").Text(),
Metrics.ForProperty("price").Number(),
Metrics.ForProperty("stockLevel").Integer(),
Metrics.ForProperty("inStock").Boolean(),
Metrics.ForProperty("rating").Number()
]
);
var typed = result.ToTyped<CategoryDashboard>();
return typed.Groups.ToDictionary(
g => g.GroupedBy.Value?.ToString() ?? "Unknown",
g => g.Properties
);
}
// Usage
var dashboard = await GetCategoryDashboard(collection);
foreach (var (category, stats) in dashboard)
{
Console.WriteLine($"Category: {category}");
Console.WriteLine($" Products: {stats.ProductCount}");
Console.WriteLine($" Price Range: ${stats.PriceMin:F2} - ${stats.PriceMax:F2}");
Console.WriteLine($" Avg Price: ${stats.PriceMean:F2}");
Console.WriteLine($" Total Stock: {stats.StockSum}");
Console.WriteLine($" Availability: {stats.InStockPercentageTrue:P0}");
Console.WriteLine($" Avg Rating: {stats.RatingMean:F1}/5");
}Analyze blog posts or articles by publication status and author:
public class ContentMetrics
{
// Publication stats
public double? IsPublishedPercentageTrue { get; set; }
public long? ViewCountSum { get; set; }
public double? ViewCountMean { get; set; }
// Content stats
public long? WordCountSum { get; set; }
public double? WordCountMean { get; set; }
// Engagement
public long? CommentCountSum { get; set; }
public double? RatingMean { get; set; }
// Categories
public Aggregate.Text? Category { get; set; }
}
// Overall content metrics
var overallResult = await collection.Aggregate.OverAll(
returnMetrics: [
Metrics.ForProperty("isPublished").Boolean(),
Metrics.ForProperty("viewCount").Integer(),
Metrics.ForProperty("wordCount").Integer(),
Metrics.ForProperty("commentCount").Integer(),
Metrics.ForProperty("rating").Number(),
Metrics.ForProperty("category").Text(topOccurrencesCount: 10)
]
);
var overall = overallResult.ToTyped<ContentMetrics>();
Console.WriteLine("=== Overall Content Metrics ===");
Console.WriteLine($"Published: {overall.Properties.IsPublishedPercentageTrue:P0}");
Console.WriteLine($"Total views: {overall.Properties.ViewCountSum:N0}");
Console.WriteLine($"Total words written: {overall.Properties.WordCountSum:N0}");
Console.WriteLine($"Average article length: {overall.Properties.WordCountMean:N0} words");
Console.WriteLine($"Average rating: {overall.Properties.RatingMean:F1}/5");
// Top categories
Console.WriteLine("\nTop Categories:");
foreach (var cat in overall.Properties.Category?.TopOccurrences ?? [])
{
Console.WriteLine($" {cat.Value}: {cat.Count} articles");
}
// Per-author breakdown
var byAuthorResult = await collection.Aggregate.OverAll(
groupBy: new Aggregate.GroupBy("author"),
returnMetrics: [
Metrics.ForProperty("isPublished").Boolean(),
Metrics.ForProperty("viewCount").Integer(),
Metrics.ForProperty("wordCount").Integer(),
Metrics.ForProperty("rating").Number()
]
);
var byAuthor = byAuthorResult.ToTyped<ContentMetrics>();
Console.WriteLine("\n=== Top Authors by Views ===");
var topAuthors = byAuthor.Groups
.OrderByDescending(g => g.Properties.ViewCountSum ?? 0)
.Take(5);
foreach (var author in topAuthors)
{
Console.WriteLine($"{author.GroupedBy.Value}:");
Console.WriteLine($" Articles: {author.TotalCount}");
Console.WriteLine($" Total views: {author.Properties.ViewCountSum:N0}");
Console.WriteLine($" Avg views: {author.Properties.ViewCountMean:N0}");
}Analyze data grouped by time periods:
public class TimeSeriesStats
{
public long? OrderCount { get; set; }
public double? RevenueSum { get; set; }
public double? RevenueMean { get; set; }
public long? ItemsSoldSum { get; set; }
public double? DiscountAppliedPercentageTrue { get; set; }
}
// Monthly revenue analysis
var monthlyResult = await collection.Aggregate.OverAll(
groupBy: new Aggregate.GroupBy("orderMonth"), // Assuming pre-computed month field
returnMetrics: [
Metrics.ForProperty("orderId").Text(),
Metrics.ForProperty("revenue").Number(),
Metrics.ForProperty("itemCount").Integer(),
Metrics.ForProperty("discountApplied").Boolean()
]
);
var monthly = monthlyResult.ToTyped<TimeSeriesStats>();
// Sort by month and display
var sortedMonths = monthly.Groups
.OrderBy(g => g.GroupedBy.Value)
.ToList();
Console.WriteLine("Monthly Revenue Report:");
Console.WriteLine("Month\t\tOrders\t\tRevenue\t\tAvg Order");
Console.WriteLine(new string('-', 60));
foreach (var month in sortedMonths)
{
Console.WriteLine($"{month.GroupedBy.Value}\t" +
$"{month.Properties.OrderCount}\t\t" +
$"${month.Properties.RevenueSum:N0}\t\t" +
$"${month.Properties.RevenueMean:N2}");
}
// Calculate growth
if (sortedMonths.Count >= 2)
{
var lastMonth = sortedMonths[^1].Properties.RevenueSum ?? 0;
var prevMonth = sortedMonths[^2].Properties.RevenueSum ?? 0;
var growth = prevMonth > 0 ? (lastMonth - prevMonth) / prevMonth : 0;
Console.WriteLine($"\nMonth-over-month growth: {growth:P1}");
}Track inventory levels and identify issues:
public class InventoryStats
{
public long? ProductCount { get; set; }
public long? StockLevelSum { get; set; }
public long? StockLevelMin { get; set; }
public double? StockLevelMean { get; set; }
public double? InStockPercentageTrue { get; set; }
public double? NeedsReorderPercentageTrue { get; set; }
}
var warehouseResult = await collection.Aggregate.OverAll(
groupBy: new Aggregate.GroupBy("warehouse"),
returnMetrics: [
Metrics.ForProperty("sku").Text(),
Metrics.ForProperty("stockLevel").Integer(),
Metrics.ForProperty("inStock").Boolean(),
Metrics.ForProperty("needsReorder").Boolean()
]
);
var byWarehouse = warehouseResult.ToTyped<InventoryStats>();
Console.WriteLine("Warehouse Inventory Summary:");
foreach (var warehouse in byWarehouse.Groups.OrderBy(g => g.GroupedBy.Value))
{
var stats = warehouse.Properties;
Console.WriteLine($"\n{warehouse.GroupedBy.Value}:");
Console.WriteLine($" SKUs: {stats.ProductCount}");
Console.WriteLine($" Total units: {stats.StockLevelSum:N0}");
Console.WriteLine($" Avg stock per SKU: {stats.StockLevelMean:F0}");
Console.WriteLine($" In stock rate: {stats.InStockPercentageTrue:P0}");
if (stats.NeedsReorderPercentageTrue > 0.2)
{
Console.WriteLine($" ⚠️ {stats.NeedsReorderPercentageTrue:P0} of items need reorder!");
}
}
// Identify warehouses with issues
var lowStockWarehouses = byWarehouse.Groups
.Where(g => g.Properties.InStockPercentageTrue < 0.8)
.ToList();
if (lowStockWarehouses.Any())
{
Console.WriteLine("\n⚠️ Warehouses needing attention:");
foreach (var wh in lowStockWarehouses)
{
Console.WriteLine($" - {wh.GroupedBy.Value}: only {wh.Properties.InStockPercentageTrue:P0} in stock");
}
}Analyze customer feedback data:
public class FeedbackStats
{
public long? ResponseCount { get; set; }
public double? SatisfactionMean { get; set; }
public double? NpsMean { get; set; }
public double? WouldRecommendPercentageTrue { get; set; }
public Aggregate.Text? TopIssues { get; set; }
}
var feedbackResult = await collection.Aggregate.OverAll(
groupBy: new Aggregate.GroupBy("productLine"),
returnMetrics: [
Metrics.ForProperty("responseId").Text(),
Metrics.ForProperty("satisfactionScore").Number(),
Metrics.ForProperty("npsScore").Number(),
Metrics.ForProperty("wouldRecommend").Boolean(),
Metrics.ForProperty("issueCategory").Text(topOccurrencesCount: 5)
]
);
var feedback = feedbackResult.ToTyped<FeedbackStats>();
Console.WriteLine("Customer Feedback Analysis by Product Line:");
foreach (var product in feedback.Groups.OrderByDescending(g => g.Properties.SatisfactionMean))
{
var stats = product.Properties;
Console.WriteLine($"\n{product.GroupedBy.Value}:");
Console.WriteLine($" Responses: {stats.ResponseCount}");
Console.WriteLine($" Satisfaction: {stats.SatisfactionMean:F1}/5");
Console.WriteLine($" NPS: {stats.NpsMean:F0}");
Console.WriteLine($" Would recommend: {stats.WouldRecommendPercentageTrue:P0}");
if (stats.TopIssues?.TopOccurrences.Any() == true)
{
Console.WriteLine(" Top reported issues:");
foreach (var issue in stats.TopIssues.TopOccurrences.Take(3))
{
Console.WriteLine($" - {issue.Value}: {issue.Count} reports");
}
}
}The Weaviate client includes a Roslyn analyzer that validates types used with ToTyped<T>() and MetricsExtractor.FromType<T>(). When you call either of these methods, the analyzer checks the type T for correct suffix usage:
public class ProductStats
{
public double? PriceMean { get; set; } // Valid - suffix with correct type
public long? QuantitySum { get; set; } // Valid - suffix with correct type
public Aggregate.Text? Category { get; set; } // Valid - full aggregate type
}
// The analyzer validates ProductStats when ToTyped<T>() or MetricsExtractor.FromType<T>() is called:
var stats = result.ToTyped<ProductStats>();
var metrics = MetricsExtractor.FromType<ProductStats>();
// If ProductStats had invalid properties, you'd see warnings:
// - Property 'Price' with no suffix: WEAVIATE002
// - Property 'PriceSum' with wrong type (string instead of numeric): WEAVIATE003The analyzer produces these diagnostics at the ToTyped<T>() or MetricsExtractor.FromType<T>() call site:
- WEAVIATE002: Warning when a property has a primitive type but no recognized suffix
- WEAVIATE003: Warning when a suffix is used with an incompatible type (e.g.,
TitleSumfor a text field)