LabelKit is a .NET toolkit for parsing kubernetes-like label-selectors and using them to build filters, including query-expressions for EFCore (PostgreSQL, MySql, SqlServer, Sqlite). Labels are name/value pairs that can be represented as dictionaries (name->value) or simple string collections (name+delimiter+value).
The majority of packages support netstandard2.0.
The following NuGet packages are provided:
- LabelKit
- You need to reference structured label-selectors.
- LabelKit.Parser
- You need to parse raw label-selectors.
- LabelKit.Expressions
- You need to build expressions and filter queries. See here.
- LabelKit.EFCore.PostgreSQL
- You need to filter EFCore-PostgreSQL queries. See here.
- LabelKit.EFCore.Pomelo.MySql
- You need to filter EFCore-MySql queries. See here.
public class Entity
{
// Stored as JSONB
public Dictionary<string, string> Labels { get; set; }
}
dotnet add package LabelKit.Parser
LabelKit.Parser offers a default parser built with Pidgin.
The parser is able to parse raw label-selectors adhering to the kubernetes syntax.
using LabelKit;
var selector = LabelSelectorParser.Parse(
"label1 = value, label2 = value, label3 in (value1, value2)");
dotnet add package LabelKit.EFCore.PostgreSQL
var expressionBuilder = NpgsqlLabelSelectorExpressionBuilders.Jsonb<Dictionary<string, string>>();
var entities = await dbContext.Set<Entity>()
.MatchLabels(e => e.Labels, selector, expressionBuilder)
.ToListAsync();
Executed SQL:
-- @__json_1='{"label1":"value","label2":"value"}' (DbType = Object)
-- @__Format_2='{"label3":"value1"}' (DbType = Object)
-- @__Format_3='{"label3":"value2"}' (DbType = Object)
SELECT t."Id", t."Labels"
FROM "TestEntity" AS t
WHERE t."Labels" @> @__json_1 AND (t."Labels" @> @__Format_2 OR t."Labels" @> @__Format_3)
Label-selectors can be easily created and extended...
var selector = LabelSelector.New()
.Match("label1").Exact("value")
.Match("label2").Not("value")
.Match("label3").In("value1", "value2")
.Match("label4").NotIn("value1", "value2")
.Match("label5").Exists()
.Match("label6").NotExists();
They can be merged...
var selector1 = LabelSelector.New()
.Match("label1").Exact("value");
var selector2 = LabelSelector.New()
.Match("label2").Exact("value");
// Contains a fully copy of all expressions
var merged = selector1.Merge(selector2);
// You can merge an arbitrary number of selectors
merged = LabelSelector.Merge(selector1, selector2, ...);
They can be rendered...
// label1 = value, label2 = value
merged.ToString();
You can also use label-selectors offline without any database interaction.
dotnet add package LabelKit
var selector = LabelSelector.New()
.Match("label1").Exact("value")
.Match("label2").Exact("value");
string[] labels = [ "label1:value", "label2:value" ];
// Default delimiter is ':'
// -> true
var doesMatch = selector.Matches(labels);
The same can be done with dictionary-like labels:
var selector = LabelSelector.New()
.Match("label1").Exact("value")
.Match("label2").Exact("value");
var labels = new Dictionary<string, string()
{
["label1"] = "value",
["label2"] = "value"
};
// -> true
var doesMatch = selector.Matches(labels);
Tip
Any component implementing ILabelSelector (meaning it can provide a collection of selector-expressions) can be used to match offline.
LabelKit supports infrastructure for building expression-trees that can be used to filter IQueryables. Components implementing ILabelSelectorExpressionBuilder are responsible for creating Expressions from label-selectors. Different expression-builders are needed for different scenarios.
Example: If your labels are stored as JSONB (PostgreSQL), the resulting SQL query has to be vastly different from if they were stored as an array. Therefore, you need a different expression.
- PostgreSQL (>=5.0.5)
- Supports labels stored as JSONB ("name": "value")
- Supports labels stored as primitive-collection of strings ("name{separator}value").
- MySql (>=3.2.0)
- Supports labels stored as JSON ("name": "value")
- Supports labels stored as primitive-collection of strings ("name{separator}value").
- Disabled by default in the provider due to this issue here.
- SqlServer (>=8.0.0)
- Supports labels stored as primitive-collection of strings ("name{separator}value").
- Sqlite (>=8.0.0)
- Supports labels stored as primitive-collection of strings ("name{separator}value").
- Any other EFCore provider supporting primitive-collections
- Supports labels stored as primitive-collection of strings ("name{separator}value").
NpgsqlJsonbLabelSelectorExpressionBuilder
(NpgsqlLabelSelectorExpressionBuilders.Jsonb()
)- Builds expression specifically suited for PostgreSQL JSONB columns.
MySqlJsonLabelSelectorExpressionBuilder
(MySqlLabelSelectorExpressionBuilders.Json()
)- Builds expression specifically suited for MySql JSON columns.
CollectionLabelSelectorExpressionBuilder
(LabelSelectorExpressionBuilders.Collection()
)- Builds generic expression suitable for any collection of strings.
- Supported by PostgreSQL, SqlServer, Sqlite (and MySql).
- Available in package
LabelKit.Expressions
.
Important
CollectionLabelSelectorExpressionBuilder
produces expressions that should be translatable to SQL by EFCore
providers supporting primitive-collections.
However, you should also be able to use those expressions offline (compiled).
dotnet add package LabelKit.Expressions
using LabelKit;
var expressionBuilder = LabelSelectorExpressionBuilders.Collection<string[]>();
// e => e.Labels is an expression representing the labels to match.
// You can also mark your entity as ILabelledEntity to avoid having to specify this every time.
var entities = await dbContext.Set<Entity>()
.MatchLabels(e => e.Labels,
selector => selector
.Match("label1").Exact("value")
.Match("label2").Exact("value")
, expressionBuilder)
.ToListAsync();