Publish - 2026-05-22 23:49:24 UTC

This commit is contained in:
Duende Bot 2026-05-22 23:49:50 +00:00
parent c2118d4733
commit 15939b5fa6
137 changed files with 1492 additions and 38 deletions

View file

@ -19,6 +19,7 @@
<PackageVersion Include="Duende.IdentityServer" Version="7.4.6" />
<PackageVersion Include="Duende.Private.Licensing" Version="1.1.0" />
<PackageVersion Include="Duende.RazorSlices" Version="1.0.0" />
<PackageVersion Include="Duende.Storage" Version="0.0.2" />
<PackageVersion Include="Google.Protobuf" Version="3.34.0" />
<PackageVersion Include="Grpc.AspNetCore" Version="2.76.0" />
<PackageVersion Include="Grpc.AspNetCore.Web" Version="2.76.0" />

View file

@ -1,8 +1,11 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
namespace Duende.Storage.Internal;
namespace Duende.Storage;
/// <summary>
/// Represents the result of a database schema version check.
/// </summary>
public sealed class CheckSchemaVersionResult
{
internal CheckSchemaVersionResult(uint currentVersion, uint requiredVersion)
@ -11,9 +14,18 @@ public sealed class CheckSchemaVersionResult
RequiredVersion = requiredVersion;
}
/// <summary>
/// Gets the current schema version in the database.
/// </summary>
public uint CurrentVersion { get; }
/// <summary>
/// Gets the schema version required by the application.
/// </summary>
public uint RequiredVersion { get; }
/// <summary>
/// Gets a value indicating whether the current schema version is compatible with the required version.
/// </summary>
public bool IsCompatible => CurrentVersion == RequiredVersion;
}

View file

@ -3,15 +3,28 @@
namespace Duende.Storage;
/// <summary>
/// Represents a version number for the database schema.
/// </summary>
public sealed record DatabaseSchemaVersion
{
/// <summary>
/// Gets the numeric version value.
/// </summary>
public int Value { get; }
/// <summary>
/// Initializes a new instance of <see cref="DatabaseSchemaVersion"/> with the specified version number.
/// </summary>
/// <param name="value">The version number. Must be non-negative.</param>
public DatabaseSchemaVersion(int value)
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
Value = value;
}
/// <summary>
/// A schema version representing no schema (version zero).
/// </summary>
public static readonly DatabaseSchemaVersion Zero = new(0);
}

View file

@ -3,12 +3,35 @@
namespace Duende.Storage.EntityAttributeValue;
/// <summary>
/// Defines an attribute's metadata including its code, type, description, uniqueness, tags, grouping, and ordering.
/// </summary>
public sealed record AttributeDefinition
{
/// <summary>
/// Initializes a new instance of the <see cref="AttributeDefinition"/> class.
/// </summary>
public AttributeDefinition()
{
}
/// <summary>
/// Reconstitutes an <see cref="AttributeDefinition"/> from persisted data without re-running constructor validation.
/// </summary>
/// <param name="code">The attribute code.</param>
/// <param name="attributeType">The attribute type descriptor.</param>
/// <param name="description">The optional description.</param>
/// <param name="displayName">The optional display name.</param>
/// <param name="isUnique">Whether the attribute value must be unique.</param>
/// <param name="isQueryable">Whether the attribute can be used in queries.</param>
/// <param name="isRequired">Whether the attribute is required.</param>
/// <param name="tags">Tags associated with the attribute.</param>
/// <param name="groupCode">The optional group code.</param>
/// <param name="order">The sort order.</param>
/// <returns>A new <see cref="AttributeDefinition"/> instance.</returns>
/// <remarks>
/// This method is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public static AttributeDefinition Load(
AttributeCode code,
AttributeType attributeType,
@ -34,6 +57,9 @@ public sealed record AttributeDefinition
Order = order
};
/// <summary>
/// The programmatic identifier for this attribute.
/// </summary>
public required AttributeCode Code { get; init; }
/// <summary>
@ -51,7 +77,14 @@ public sealed record AttributeDefinition
: throw new InvalidOperationException(
$"Attribute '{Code}' has type '{AttributeType.GetType().Name}', not a scalar type. Use AttributeType instead.");
/// <summary>
/// An optional human-readable description of the attribute.
/// </summary>
public AttributeDescription? Description { get; init; }
/// <summary>
/// An optional human-readable display name for the attribute.
/// </summary>
public AttributeDisplayName? DisplayName { get; init; }
/// <summary>
@ -66,8 +99,19 @@ public sealed record AttributeDefinition
/// </remarks>
public bool IsQueryable { get; init; } = true;
/// <summary>
/// Indicates whether a value for this attribute is required.
/// </summary>
public bool IsRequired { get; init; }
/// <summary>
/// Indicates whether values of this attribute must be unique across entities.
/// </summary>
public bool IsUnique { get; init; }
/// <summary>
/// Tags associated with this attribute for categorization or filtering.
/// </summary>
public IReadOnlyCollection<string> Tags { get; init; } = [];
/// <summary>
@ -81,6 +125,10 @@ public sealed record AttributeDefinition
/// </summary>
public int Order { get; init; }
/// <summary>
/// Implicitly converts an <see cref="AttributeDefinition"/> to its <see cref="AttributeCode"/>.
/// </summary>
/// <param name="definition">The attribute definition.</param>
#pragma warning disable CA2225 // Operator overloads have named alternates
public static implicit operator AttributeCode(AttributeDefinition definition)
{

View file

@ -8,6 +8,13 @@ namespace Duende.Storage.EntityAttributeValue;
/// </summary>
public sealed record AttributeGroup
{
/// <summary>
/// Creates a new attribute group with the specified metadata.
/// </summary>
/// <param name="Code">The unique code identifying this group.</param>
/// <param name="DisplayName">An optional human-readable display name.</param>
/// <param name="Description">An optional description of the group.</param>
/// <param name="Order">The sort order for display purposes.</param>
public AttributeGroup(
AttributeGroupCode Code,
AttributeDisplayName? DisplayName,
@ -20,8 +27,23 @@ public sealed record AttributeGroup
this.Order = Order;
}
/// <summary>
/// The unique code identifying this group.
/// </summary>
public AttributeGroupCode Code { get; init; }
/// <summary>
/// An optional human-readable display name for the group.
/// </summary>
public AttributeDisplayName? DisplayName { get; init; }
/// <summary>
/// An optional description of the group.
/// </summary>
public AttributeDescription? Description { get; init; }
/// <summary>
/// The sort order for display purposes.
/// </summary>
public int Order { get; init; }
}

View file

@ -6,15 +6,32 @@ using System.Diagnostics.CodeAnalysis;
namespace Duende.Storage.EntityAttributeValue;
/// <summary>
/// Represents a read-only, validated attribute value paired with its attribute code.
/// Instances can only be created via an <see cref="AttributeValueCollection"/> (which validates against a schema)
/// or by reconstituting from previously persisted data using <see cref="Load{T}"/>.
/// </summary>
#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
public abstract record AttributeValue
{
private protected AttributeValue(AttributeCode code) => Code = code;
/// <summary>
/// The attribute code identifying which attribute this value belongs to.
/// </summary>
public AttributeCode Code { get; }
/// <summary>
/// Gets the value as an untyped object.
/// </summary>
public abstract object UntypedValue { get; }
/// <summary>
/// Attempts to retrieve the value as the specified type.
/// </summary>
/// <typeparam name="T">The expected value type.</typeparam>
/// <param name="value">The typed value if successful.</param>
/// <returns><c>true</c> if the value is of the specified type; otherwise, <c>false</c>.</returns>
public bool TryGetValue<T>([MaybeNullWhen(false)] out T value)
{
if (this is AttributeValue<T> typed)
@ -27,11 +44,25 @@ public abstract record AttributeValue
return false;
}
/// <summary>
/// Returns the string representation of the value.
/// </summary>
public override string ToString() => UntypedValue.ToString()!;
/// <summary>
/// Reconstitutes a typed attribute value from persisted data.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="code">The attribute code.</param>
/// <param name="value">The attribute value.</param>
/// <returns>A new typed <see cref="AttributeValue{T}"/> instance.</returns>
public static AttributeValue<T> Load<T>(AttributeCode code, T value) => new(code, value);
}
/// <summary>
/// Represents a strongly-typed attribute value paired with its attribute code.
/// </summary>
/// <typeparam name="T">The type of the attribute value.</typeparam>
public sealed record AttributeValue<T> : AttributeValue
{
internal AttributeValue(AttributeCode code, T value) : base(code) =>
@ -43,8 +74,14 @@ public sealed record AttributeValue<T> : AttributeValue
_ => value
};
/// <summary>
/// Gets the strongly-typed value.
/// </summary>
public T TypedValue { get; }
/// <summary>
/// Gets the value as an untyped object.
/// </summary>
public override object UntypedValue => TypedValue!;
internal static AttributeValue<T> Load(AttributeCode code, T value) => new(code, value);

View file

@ -7,17 +7,29 @@ using Duende.Storage.EntityAttributeValue.Internal;
namespace Duende.Storage.EntityAttributeValue;
/// <summary>
/// A mutable collection of attribute values validated against an attribute schema.
/// </summary>
public sealed class AttributeValueCollection : IEnumerable<AttributeValue>
{
private readonly Dictionary<AttributeCode, AttributeValue> _dict = [];
private readonly IReadOnlyAttributeSchema? _schema;
/// <summary>
/// Creates an empty collection backed by the specified schema.
/// </summary>
/// <param name="schema">The attribute schema used for validation.</param>
public AttributeValueCollection(IReadOnlyAttributeSchema schema)
{
ArgumentNullException.ThrowIfNull(schema);
_schema = schema;
}
/// <summary>
/// Creates a collection pre-populated with the specified attributes, validated against the schema.
/// </summary>
/// <param name="schema">The attribute schema used for validation.</param>
/// <param name="attributes">The initial attribute values to populate.</param>
public AttributeValueCollection(IReadOnlyAttributeSchema schema, IEnumerable<AttributeValue> attributes)
{
ArgumentNullException.ThrowIfNull(schema);
@ -46,6 +58,10 @@ public sealed class AttributeValueCollection : IEnumerable<AttributeValue>
}
}
/// <summary>
/// Sets an attribute value in the collection, replacing any existing value for the same code.
/// </summary>
/// <param name="attribute">The attribute value to set.</param>
public void Set(AttributeValue attribute)
{
ArgumentNullException.ThrowIfNull(attribute);
@ -68,24 +84,45 @@ public sealed class AttributeValueCollection : IEnumerable<AttributeValue>
_dict[attribute.Code] = attribute;
}
/// <summary>Sets a boolean attribute value.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The boolean value.</param>
public void Set(AttributeCode code, bool value) =>
SetTyped(code, value, ScalarDataType.Boolean);
/// <summary>Sets an integer attribute value.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The integer value.</param>
public void Set(AttributeCode code, int value) =>
SetTyped(code, value, ScalarDataType.Integer);
/// <summary>Sets a decimal attribute value.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The decimal value.</param>
public void Set(AttributeCode code, decimal value) =>
SetTyped(code, value, ScalarDataType.Decimal);
/// <summary>Sets a string attribute value.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The string value.</param>
public void Set(AttributeCode code, string value) =>
SetTyped(code, value, ScalarDataType.String);
/// <summary>Sets a date attribute value.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The date value.</param>
public void Set(AttributeCode code, DateOnly value) =>
SetTyped(code, value, ScalarDataType.Date);
/// <summary>Sets a date-time attribute value.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The date-time value.</param>
public void Set(AttributeCode code, DateTimeOffset value) =>
SetTyped(code, value, ScalarDataType.DateTime);
/// <summary>Sets a complex (dictionary) attribute value.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The complex value as a dictionary of properties.</param>
public void Set(AttributeCode code, IReadOnlyDictionary<string, object> value)
{
if (_schema != null)
@ -96,6 +133,9 @@ public sealed class AttributeValueCollection : IEnumerable<AttributeValue>
_dict[code] = new AttributeValue<IReadOnlyDictionary<string, object>>(code, value);
}
/// <summary>Sets a list attribute value.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The list value.</param>
public void Set(AttributeCode code, IReadOnlyList<object> value)
{
if (_schema != null)
@ -106,24 +146,59 @@ public sealed class AttributeValueCollection : IEnumerable<AttributeValue>
_dict[code] = new AttributeValue<IReadOnlyList<object>>(code, value);
}
/// <summary>Attempts to set a boolean value, returning errors on failure.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The boolean value.</param>
/// <param name="errors">Validation errors if the operation fails.</param>
/// <returns><c>true</c> if the value was set successfully; otherwise, <c>false</c>.</returns>
public bool TrySet(AttributeCode code, bool value, [NotNullWhen(false)] out IReadOnlyList<string>? errors) =>
TrySetTyped(code, value, ScalarDataType.Boolean, out errors);
/// <summary>Attempts to set an integer value, returning errors on failure.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The integer value.</param>
/// <param name="errors">Validation errors if the operation fails.</param>
/// <returns><c>true</c> if the value was set successfully; otherwise, <c>false</c>.</returns>
public bool TrySet(AttributeCode code, int value, [NotNullWhen(false)] out IReadOnlyList<string>? errors) =>
TrySetTyped(code, value, ScalarDataType.Integer, out errors);
/// <summary>Attempts to set a decimal value, returning errors on failure.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The decimal value.</param>
/// <param name="errors">Validation errors if the operation fails.</param>
/// <returns><c>true</c> if the value was set successfully; otherwise, <c>false</c>.</returns>
public bool TrySet(AttributeCode code, decimal value, [NotNullWhen(false)] out IReadOnlyList<string>? errors) =>
TrySetTyped(code, value, ScalarDataType.Decimal, out errors);
/// <summary>Attempts to set a string value, returning errors on failure.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The string value.</param>
/// <param name="errors">Validation errors if the operation fails.</param>
/// <returns><c>true</c> if the value was set successfully; otherwise, <c>false</c>.</returns>
public bool TrySet(AttributeCode code, string value, [NotNullWhen(false)] out IReadOnlyList<string>? errors) =>
TrySetTyped(code, value, ScalarDataType.String, out errors);
/// <summary>Attempts to set a date value, returning errors on failure.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The date value.</param>
/// <param name="errors">Validation errors if the operation fails.</param>
/// <returns><c>true</c> if the value was set successfully; otherwise, <c>false</c>.</returns>
public bool TrySet(AttributeCode code, DateOnly value, [NotNullWhen(false)] out IReadOnlyList<string>? errors) =>
TrySetTyped(code, value, ScalarDataType.Date, out errors);
/// <summary>Attempts to set a date-time value, returning errors on failure.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The date-time value.</param>
/// <param name="errors">Validation errors if the operation fails.</param>
/// <returns><c>true</c> if the value was set successfully; otherwise, <c>false</c>.</returns>
public bool TrySet(AttributeCode code, DateTimeOffset value, [NotNullWhen(false)] out IReadOnlyList<string>? errors) =>
TrySetTyped(code, value, ScalarDataType.DateTime, out errors);
/// <summary>Attempts to set a complex (dictionary) value, returning errors on failure.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The complex value.</param>
/// <param name="errors">Validation errors if the operation fails.</param>
/// <returns><c>true</c> if the value was set successfully; otherwise, <c>false</c>.</returns>
public bool TrySet(AttributeCode code, IReadOnlyDictionary<string, object> value, [NotNullWhen(false)] out IReadOnlyList<string>? errors)
{
if (_schema != null)
@ -142,6 +217,11 @@ public sealed class AttributeValueCollection : IEnumerable<AttributeValue>
return true;
}
/// <summary>Attempts to set a list value, returning errors on failure.</summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The list value.</param>
/// <param name="errors">Validation errors if the operation fails.</param>
/// <returns><c>true</c> if the value was set successfully; otherwise, <c>false</c>.</returns>
public bool TrySet(AttributeCode code, IReadOnlyList<object> value, [NotNullWhen(false)] out IReadOnlyList<string>? errors)
{
if (_schema != null)
@ -160,6 +240,10 @@ public sealed class AttributeValueCollection : IEnumerable<AttributeValue>
return true;
}
/// <summary>
/// Validates the collection against the schema and returns an immutable validated collection.
/// </summary>
/// <returns>A validated, immutable attribute value collection.</returns>
public ValidatedAttributeValueCollection Validate()
{
if (_schema == null)
@ -181,6 +265,12 @@ public sealed class AttributeValueCollection : IEnumerable<AttributeValue>
return new ValidatedAttributeValueCollection(_dict.Values, schema.SchemaId, schema.Version);
}
/// <summary>
/// Attempts to validate the collection, returning the validated collection or errors.
/// </summary>
/// <param name="validated">The validated collection if successful.</param>
/// <param name="errors">Validation errors if the operation fails.</param>
/// <returns><c>true</c> if validation succeeds; otherwise, <c>false</c>.</returns>
public bool TryValidate([NotNullWhen(true)] out ValidatedAttributeValueCollection? validated, [NotNullWhen(false)] out IReadOnlyList<string>? errors)
{
if (_schema == null)
@ -208,8 +298,16 @@ public sealed class AttributeValueCollection : IEnumerable<AttributeValue>
return true;
}
/// <summary>
/// Gets the number of attribute values in the collection.
/// </summary>
public int Count => _dict.Count;
/// <summary>
/// Removes the attribute value with the specified code.
/// </summary>
/// <param name="code">The attribute code to remove.</param>
/// <returns><c>true</c> if the attribute was removed; otherwise, <c>false</c>.</returns>
public bool Remove(AttributeCode code)
{
if (_schema != null &&
@ -222,15 +320,35 @@ public sealed class AttributeValueCollection : IEnumerable<AttributeValue>
return _dict.Remove(code);
}
/// <summary>
/// Determines whether an attribute with the specified code exists in the collection.
/// </summary>
/// <param name="code">The attribute code to check.</param>
/// <returns><c>true</c> if the attribute exists; otherwise, <c>false</c>.</returns>
public bool Contains(AttributeCode code) => _dict.ContainsKey(code);
/// <summary>
/// Attempts to retrieve an attribute value by code.
/// </summary>
/// <param name="code">The attribute code to look up.</param>
/// <param name="attribute">The attribute value if found.</param>
/// <returns><c>true</c> if the attribute was found; otherwise, <c>false</c>.</returns>
public bool TryGet(AttributeCode code, [MaybeNullWhen(false)] out AttributeValue attribute) =>
_dict.TryGetValue(code, out attribute);
/// <summary>
/// Gets the attribute value with the specified code.
/// </summary>
/// <param name="code">The attribute code.</param>
/// <returns>The attribute value.</returns>
#pragma warning disable CA1043 // Use integral or string argument for indexers
public AttributeValue this[AttributeCode code] => _dict[code];
#pragma warning restore CA1043
/// <summary>
/// Returns an enumerator that iterates through the attribute values.
/// </summary>
/// <returns>An enumerator for the collection.</returns>
public IEnumerator<AttributeValue> GetEnumerator() => _dict.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

View file

@ -10,6 +10,10 @@ namespace Duende.Storage.EntityAttributeValue;
/// </summary>
public sealed record ComplexAttributeType : AttributeType
{
/// <summary>
/// Creates a complex attribute type with the specified named properties.
/// </summary>
/// <param name="Properties">The property definitions keyed by attribute code.</param>
public ComplexAttributeType(IReadOnlyDictionary<AttributeCode, ComplexAttributeProperty> Properties)
{
ArgumentNullException.ThrowIfNull(Properties, nameof(Properties));
@ -72,6 +76,11 @@ public sealed record ComplexAttributeType : AttributeType
return false;
}
/// <summary>
/// Determines whether this complex type is equal to another.
/// </summary>
/// <param name="other">The other complex attribute type.</param>
/// <returns><c>true</c> if the types are equal; otherwise, <c>false</c>.</returns>
public bool Equals(ComplexAttributeType? other)
{
if (other is null)
@ -95,6 +104,10 @@ public sealed record ComplexAttributeType : AttributeType
return true;
}
/// <summary>
/// Returns the hash code for this complex attribute type.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
var hash = new HashCode();

View file

@ -3,8 +3,14 @@
namespace Duende.Storage.EntityAttributeValue;
/// <summary>
/// Provides a read-only view of attribute definitions and groups in a schema.
/// </summary>
public interface IReadOnlyAttributeSchema
{
/// <summary>
/// The attribute definitions in this schema, keyed by attribute code.
/// </summary>
IReadOnlyDictionary<AttributeCode, AttributeDefinition> AttributeDefinitions { get; }
/// <summary>

View file

@ -3,8 +3,27 @@
namespace Duende.Storage.EntityAttributeValue.Internal.Storage;
public static class AttributeDefinitionDso
/// <summary>
/// Provides the persisted data storage object representation of an attribute definition.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
internal static class AttributeDefinitionDso
{
/// <summary>
/// Version 1 of the attribute definition data storage object.
/// </summary>
/// <param name="Code">The attribute code.</param>
/// <param name="Type">The attribute type descriptor.</param>
/// <param name="Description">The description, or null.</param>
/// <param name="IsUnique">Whether the attribute value must be unique.</param>
/// <param name="Tags">The tags associated with the attribute.</param>
/// <param name="GroupCode">The group code, or null if ungrouped.</param>
/// <param name="Order">The sort order.</param>
/// <param name="DisplayName">The display name, or null.</param>
/// <param name="IsQueryable">Whether the attribute can be used in queries.</param>
/// <param name="IsRequired">Whether the attribute is required.</param>
public sealed record V1(
string Code,
AttributeTypeDso Type,

View file

@ -3,7 +3,20 @@
namespace Duende.Storage.EntityAttributeValue.Internal.Storage;
public static class AttributeGroupDso
/// <summary>
/// Provides the persisted data storage object representation of an attribute group.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
internal static class AttributeGroupDso
{
/// <summary>
/// Version 1 of the attribute group data storage object.
/// </summary>
/// <param name="Code">The group code.</param>
/// <param name="DisplayName">The display name, or null.</param>
/// <param name="Description">The description, or null.</param>
/// <param name="Order">The sort order.</param>
public sealed record V1(string Code, string? DisplayName, string? Description, int Order);
}

View file

@ -5,12 +5,25 @@ using Duende.Storage.Internal;
namespace Duende.Storage.EntityAttributeValue.Internal.Storage;
public static class AttributeSchemaDso
/// <summary>
/// Provides the persisted data storage object representation of an attribute schema.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
internal static class AttributeSchemaDso
{
/// <summary>Gets the entity type for the attribute schema DSO.</summary>
public static readonly EntityType EntityType = new(1501, "UserProfileSchemaDso");
/// <summary>
/// Version 1 of the attribute schema data storage object.
/// </summary>
/// <param name="AttributeDefinitions">The attribute definitions.</param>
/// <param name="Groups">The attribute groups.</param>
public sealed record V1(ICollection<AttributeDefinitionDso.V1> AttributeDefinitions, ICollection<AttributeGroupDso.V1> Groups) : IDataStorageObject
{
/// <summary>Gets the data storage object version descriptor.</summary>
public static DataStorageObjectVersion DsoVersion { get; } = new(EntityType, 1);
}
}

View file

@ -4,11 +4,14 @@
namespace Duende.Storage.EntityAttributeValue.Internal.Storage;
/// <summary>
/// Persisted representation of an <see cref="AttributeType"/>.
/// <c>EnumValues</c> and <c>ConstrainedValues</c> are reserved for future enum/constrained-string
/// attribute types and are currently always <c>null</c>.
/// Persisted representation of an <see cref="AttributeType"/>.
/// <c>EnumValues</c> and <c>ConstrainedValues</c> are reserved for future enum/constrained-string
/// attribute types and are currently always <c>null</c>.
/// </summary>
public sealed record AttributeTypeDso(
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
internal sealed record AttributeTypeDso(
string Kind,
string? ScalarDataType,
IReadOnlyList<EnumValueDso>? EnumValues,
@ -17,9 +20,12 @@ public sealed record AttributeTypeDso(
AttributeTypeDso? ElementType);
/// <summary>
/// Persisted representation of a sub-property within a complex attribute type.
/// Persisted representation of a sub-property within a complex attribute type.
/// </summary>
public sealed record ComplexPropertyDso(
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
internal sealed record ComplexPropertyDso(
AttributeTypeDso Type,
string? DisplayName,
string? Description);

View file

@ -8,12 +8,15 @@ using Duende.Storage.Internal.Querying.Fields;
namespace Duende.Storage.EntityAttributeValue.Internal.Storage;
/// <summary>
/// Resolves SCIM attribute paths to Faro Field types based on the dynamic user schema.
/// Unlike other <see cref="IScimAttributeTypeResolver"/> implementations which map fixed SCIM User schema attributes,
/// Resolves query attribute paths to Faro Field types based on the dynamic user schema.
/// Unlike other <see cref="IQueryAttributeTypeResolver"/> implementations which map fixed schema attributes,
/// this resolver dynamically maps user-defined schema attributes to their Faro field types.
/// Supports dotted paths (e.g., <c>address.city</c>, <c>phones.type</c>) for complex and list types.
/// </summary>
public sealed class AttributeTypeResolver : IScimAttributeTypeResolver
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
internal sealed class AttributeTypeResolver : IQueryAttributeTypeResolver
{
private readonly IReadOnlyDictionary<AttributeCode, AttributeDefinition> _attributeDefinitions;

View file

@ -6,7 +6,13 @@ using Duende.Storage.Internal;
namespace Duende.Storage.EntityAttributeValue.Internal.Storage;
public sealed record AttributeValueDskV1 : IDataStorageKey
/// <summary>
/// Represents a version 1 data storage key for an attribute value, keyed by attribute code and value.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
internal sealed record AttributeValueDskV1 : IDataStorageKey
{
private AttributeValueDskV1(string name, string value)
{
@ -14,16 +20,30 @@ public sealed record AttributeValueDskV1 : IDataStorageKey
Value = value;
}
/// <summary>Gets the data storage key version descriptor.</summary>
public static DataStorageKeyVersion DskVersion { get; } =
new(new DataStorageKeyType(1u, "Attribute"), 1);
/// <summary>Gets the attribute name (code).</summary>
public string Name { get; }
/// <summary>Gets the attribute value as an invariant string.</summary>
public string Value { get; }
/// <summary>
/// Creates a key from an <see cref="AttributeValue"/>.
/// </summary>
/// <param name="attribute">The attribute value.</param>
/// <returns>A new data storage key.</returns>
public static AttributeValueDskV1 Create(AttributeValue attribute) =>
new(attribute.Code.Value, ToInvariantString(attribute.UntypedValue));
/// <summary>
/// Creates a key from an attribute code and value.
/// </summary>
/// <param name="code">The attribute code.</param>
/// <param name="value">The attribute value.</param>
/// <returns>A new data storage key.</returns>
public static AttributeValueDskV1 Create(AttributeCode code, object value) =>
new(code.Value, ToInvariantString(value));

View file

@ -3,7 +3,18 @@
namespace Duende.Storage.EntityAttributeValue.Internal.Storage;
public static class AttributeValueDso
/// <summary>
/// Provides the persisted data storage object representation of an attribute value.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
internal static class AttributeValueDso
{
/// <summary>
/// Version 1 of the attribute value data storage object.
/// </summary>
/// <param name="Name">The attribute name.</param>
/// <param name="Value">The attribute value, or null.</param>
public sealed record V1(string Name, object? Value);
}

View file

@ -3,4 +3,12 @@
namespace Duende.Storage.EntityAttributeValue.Internal.Storage;
public sealed record EnumValueDso(string Key, string DisplayName);
/// <summary>
/// Persisted representation of an enumeration value within an attribute type.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
/// <param name="Key">The enum value key.</param>
/// <param name="DisplayName">The human-readable display name.</param>
internal sealed record EnumValueDso(string Key, string DisplayName);

View file

@ -9,6 +9,10 @@ namespace Duende.Storage.EntityAttributeValue;
/// </summary>
public sealed record ListAttributeType : AttributeType
{
/// <summary>
/// Creates a list attribute type with the specified element type.
/// </summary>
/// <param name="ElementType">The type of elements in the list.</param>
public ListAttributeType(AttributeType ElementType)
{
ArgumentNullException.ThrowIfNull(ElementType);
@ -19,10 +23,22 @@ public sealed record ListAttributeType : AttributeType
ValidateNesting();
}
/// <summary>
/// Gets the type of elements in this list.
/// </summary>
public AttributeType ElementType { get; }
/// <summary>
/// Determines whether this list type is equal to another.
/// </summary>
/// <param name="other">The other list attribute type.</param>
/// <returns><c>true</c> if the types are equal; otherwise, <c>false</c>.</returns>
public bool Equals(ListAttributeType? other) =>
other is not null && ElementType.Equals(other.ElementType);
/// <summary>
/// Returns the hash code for this list attribute type.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode() => HashCode.Combine(ElementType);
}

View file

@ -8,6 +8,10 @@ namespace Duende.Storage.EntityAttributeValue;
/// </summary>
public sealed record ScalarAttributeType : AttributeType
{
/// <summary>
/// Creates a scalar attribute type with the specified data type.
/// </summary>
/// <param name="DataType">The scalar data type.</param>
public ScalarAttributeType(ScalarDataType DataType)
{
if (!Enum.IsDefined(DataType))
@ -18,5 +22,8 @@ public sealed record ScalarAttributeType : AttributeType
this.DataType = DataType;
}
/// <summary>
/// Gets the scalar data type for this attribute.
/// </summary>
public ScalarDataType DataType { get; init; }
}

View file

@ -9,10 +9,21 @@ namespace Duende.Storage.EntityAttributeValue;
#pragma warning disable CA1720 // Identifiers should not contain type names
public enum ScalarDataType
{
/// <summary>A boolean (true/false) value.</summary>
Boolean,
/// <summary>A date-only value without time component.</summary>
Date,
/// <summary>A date and time value with time zone offset.</summary>
DateTime,
/// <summary>A decimal numeric value.</summary>
Decimal,
/// <summary>A 32-bit integer value.</summary>
Integer,
/// <summary>A text string value.</summary>
String,
}

View file

@ -7,10 +7,16 @@ using System.Diagnostics.CodeAnalysis;
namespace Duende.Storage.EntityAttributeValue;
/// <summary>
/// An immutable, validated collection of attribute values that has passed schema validation.
/// </summary>
public sealed class ValidatedAttributeValueCollection : IEnumerable<AttributeValue>
{
private readonly FrozenDictionary<AttributeCode, AttributeValue> _dict;
/// <summary>
/// Gets an empty validated collection with no attribute values.
/// </summary>
public static ValidatedAttributeValueCollection Empty { get; } =
new([], UuidV7.Load(Guid.Empty), 0);
@ -24,17 +30,40 @@ public sealed class ValidatedAttributeValueCollection : IEnumerable<AttributeVal
internal UuidV7 SchemaId { get; }
internal int SchemaVersion { get; }
/// <summary>
/// Gets the number of attribute values in the collection.
/// </summary>
public int Count => _dict.Count;
/// <summary>
/// Determines whether an attribute with the specified code exists in the collection.
/// </summary>
/// <param name="code">The attribute code to check.</param>
/// <returns><c>true</c> if the attribute exists; otherwise, <c>false</c>.</returns>
public bool Contains(AttributeCode code) => _dict.ContainsKey(code);
/// <summary>
/// Attempts to retrieve an attribute value by code.
/// </summary>
/// <param name="code">The attribute code to look up.</param>
/// <param name="attribute">The attribute value if found.</param>
/// <returns><c>true</c> if the attribute was found; otherwise, <c>false</c>.</returns>
public bool TryGet(AttributeCode code, [MaybeNullWhen(false)] out AttributeValue attribute) =>
_dict.TryGetValue(code, out attribute);
/// <summary>
/// Gets the attribute value with the specified code.
/// </summary>
/// <param name="code">The attribute code.</param>
/// <returns>The attribute value.</returns>
#pragma warning disable CA1043 // Use integral or string argument for indexers
public AttributeValue this[AttributeCode code] => _dict[code];
#pragma warning restore CA1043
/// <summary>
/// Returns an enumerator that iterates through the attribute values.
/// </summary>
/// <returns>An enumerator for the collection.</returns>
public IEnumerator<AttributeValue> GetEnumerator() => ((IEnumerable<AttributeValue>)_dict.Values).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

View file

@ -1,10 +1,11 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.Storage.Internal;
namespace Duende.Storage;
/// <summary>
/// Provides database schema management including migrations and verification.
/// </summary>
public interface IDatabaseSchema
{
/// <summary>

View file

@ -5,7 +5,13 @@ using Microsoft.Extensions.DependencyInjection;
namespace Duende.Storage;
/// <summary>
/// A builder interface for configuring storage services.
/// </summary>
public interface IStorageBuilder
{
/// <summary>
/// Gets the service collection used to register storage services.
/// </summary>
public IServiceCollection Services { get; }
}

View file

@ -3,8 +3,21 @@
namespace Duende.Storage.Internal.Builder;
public sealed class DsoRegistration(Type dsoType, DataStorageObjectVersion dsoVersion)
/// <summary>
/// Represents a DSO type registration binding a CLR type to its DSO version.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
internal sealed class DsoRegistration(Type dsoType, DataStorageObjectVersion dsoVersion)
{
/// <summary>
/// Gets the CLR type of the DSO.
/// </summary>
public Type DsoType { get; } = dsoType;
/// <summary>
/// Gets the DSO version.
/// </summary>
public DataStorageObjectVersion DsoVersion { get; } = dsoVersion;
}

View file

@ -7,10 +7,20 @@ using Microsoft.Extensions.DependencyInjection;
namespace Duende.Storage.Internal.Builder;
#pragma warning restore IDE0130
/// <summary>
/// Extension methods for registering DSO types in the service collection.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public static class DsoRegistrationServiceCollectionExtensions
{
extension(IServiceCollection services)
{
/// <summary>
/// Registers a DSO type for deserialization support.
/// </summary>
/// <typeparam name="TDso">The DSO type to register.</typeparam>
public void AddDsoRegistration<TDso>() where TDso : IDataStorageObject
{
var dsoRegistration = new DsoRegistration(typeof(TDso), TDso.DsoVersion);

View file

@ -3,4 +3,12 @@
namespace Duende.Storage.Internal.Builder;
/// <summary>
/// Delegate that resolves the registered CLR type for a given DSO version.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
/// <param name="version">The DSO version to look up.</param>
/// <returns>The registered CLR type.</returns>
public delegate Type GetRegisteredTypeForDso(DataStorageObjectVersion version);

View file

@ -5,6 +5,12 @@ using System.Text.Json;
namespace Duende.Storage.Internal;
/// <summary>
/// Represents a key used for alternate lookups in the data store.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed class DataStorageKey
{
private DataStorageKey(DataStorageKeyVersion version, Guid value, string? keyJsonValue)
@ -14,12 +20,27 @@ public sealed class DataStorageKey
KeyJsonValue = keyJsonValue;
}
/// <summary>
/// Gets the version of the data storage key.
/// </summary>
public DataStorageKeyVersion DskVersion { get; private set; }
/// <summary>
/// Gets the GUID value of the key.
/// </summary>
public Guid Value { get; private set; }
/// <summary>
/// Gets the JSON representation of the key, or <c>null</c> for GUID-based keys.
/// </summary>
public string? KeyJsonValue { get; private set; }
/// <summary>
/// Creates a <see cref="DataStorageKey"/> from the specified key instance.
/// </summary>
/// <typeparam name="T">The type of the data storage key.</typeparam>
/// <param name="dsk">The data storage key instance.</param>
/// <returns>A new <see cref="DataStorageKey"/>.</returns>
public static DataStorageKey Create<T>(T dsk) where T : IDataStorageKey
{
if (dsk is IGuidDataStorageKey guidDsk)

View file

@ -6,19 +6,39 @@ using System.Globalization;
namespace Duende.Storage.Internal;
/// <summary>
/// The type of DSK that's being stored.
/// The type of DSK that's being stored.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
/// <param name="Id">A number representation for the DSK type.</param>
/// <param name="Name">The name of the DSK type. This name is only used for display purposes and should never change. </param>
/// <param name="Name">The name of the DSK type. This name is only used for display purposes and should never change.</param>
public readonly record struct DataStorageKeyType(uint Id, string Name)
{
/// <summary>
/// Obsolete parameterless constructor. Do not use.
/// </summary>
[Obsolete("Don't use this constructor")]
public DataStorageKeyType() : this(0!, null!) => throw new InvalidOperationException("Cannot instantiate DSKType without parameters");
/// <summary>
/// Builds a <see cref="DataStorageKeyType"/> from an enum value.
/// </summary>
/// <param name="enum">The enum value to convert.</param>
/// <returns>A <see cref="DataStorageKeyType"/> with the numeric and string representation of the enum.</returns>
public static DataStorageKeyType BuildFrom(Enum @enum) =>
new((uint)Convert.ToInt32(@enum, CultureInfo.InvariantCulture), @enum.ToString());
/// <summary>
/// Creates a <see cref="DataStorageKeyType"/> from an enum value.
/// </summary>
/// <param name="value">The enum value to convert.</param>
/// <returns>A <see cref="DataStorageKeyType"/>.</returns>
public static DataStorageKeyType FromEnum(Enum value) => BuildFrom(value);
/// <summary>
/// Implicitly converts an enum value to a <see cref="DataStorageKeyType"/>.
/// </summary>
/// <param name="value">The enum value to convert.</param>
public static implicit operator DataStorageKeyType(Enum value) => BuildFrom(value);
}

View file

@ -3,8 +3,17 @@
namespace Duende.Storage.Internal;
/// <summary>
/// Represents a versioned data storage key type, combining the key type with its schema version.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
/// <param name="KeyType">The type of the data storage key.</param>
/// <param name="SchemaVersion">The schema version of the key.</param>
public sealed record DataStorageKeyVersion(DataStorageKeyType KeyType, uint SchemaVersion)
{
/// <inheritdoc />
public override string ToString() => $"{KeyType.Name}({KeyType.Id}) v{SchemaVersion}";
}

View file

@ -6,6 +6,9 @@ namespace Duende.Storage.Internal;
/// <summary>
/// Represents a versioned DSO type.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record DataStorageObjectVersion(EntityType EntityType, uint SchemaVersion)
{
public override string ToString() => $"{EntityType.Name}({EntityType.Id}) v{SchemaVersion}";

View file

@ -6,8 +6,20 @@ using System.Text;
namespace Duende.Storage.Internal;
/// <summary>
/// Generates deterministic GUIDs from string inputs using MD5 hashing.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public static class DeterministicGuidGenerator
{
/// <summary>
/// Creates a deterministic GUID from the specified name.
/// </summary>
/// <param name="name">The name to generate a GUID from.</param>
/// <returns>A deterministic GUID derived from the input name.</returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is null or empty.</exception>
public static Guid Create(string name)
{
if (string.IsNullOrEmpty(name))

View file

@ -7,10 +7,16 @@ namespace Duende.Storage.Internal;
/// The type of document that's being stored.
/// Each library defines its own entity types as static fields on the containing DSO class.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
/// <param name="Id">A number representation for the DSO type. Must be unique across all entity types.</param>
/// <param name="Name">The name of the DSO type. Used for analytics and display purposes. Should never change once entities exist.</param>
public readonly record struct EntityType(uint Id, string Name)
{
/// <summary>
/// Obsolete parameterless constructor. Do not use.
/// </summary>
[Obsolete("Don't use this constructor")]
public EntityType() : this(0, null!) => throw new InvalidOperationException("Cannot instantiate EntityType without parameters");
}

View file

@ -9,6 +9,9 @@ namespace Duende.Storage.Internal;
/// <see cref="InRelative"/> for a duration from now,
/// or <see cref="NoExpiration"/> to explicitly indicate no expiration.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public abstract record Expiration
{
// Prevent external subclassing.

View file

@ -7,6 +7,9 @@ namespace Duende.Storage.Internal;
/// Specifies the type of a field, corresponding to the typed columns in the search_values table.
/// This ensures QueryFields reads from the correct column instead of checking the first non-null value.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public enum FieldType
{
/// <summary>

View file

@ -3,16 +3,41 @@
namespace Duende.Storage.Internal.Filtering;
/// <summary>
/// Defines the comparison operators supported by the filter expression parser.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public enum ComparisonOperator
{
/// <summary>Equality comparison.</summary>
Equal,
/// <summary>Inequality comparison.</summary>
NotEqual,
/// <summary>Substring containment.</summary>
Contains,
/// <summary>Prefix match.</summary>
StartsWith,
/// <summary>Suffix match.</summary>
EndsWith,
/// <summary>Greater than comparison.</summary>
GreaterThan,
/// <summary>Greater than or equal comparison.</summary>
GreaterThanOrEqual,
/// <summary>Less than comparison.</summary>
LessThan,
/// <summary>Less than or equal comparison.</summary>
LessThanOrEqual,
/// <summary>Presence check (field has a value).</summary>
Present
}

View file

@ -3,9 +3,18 @@
namespace Duende.Storage.Internal.Filtering.Expressions;
/// <summary>
/// Represents an attribute path reference in a filter expression.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
/// <param name="path">The attribute path string.</param>
public sealed class AttributePathExpression(string path) : FilterExpression
{
/// <summary>Gets the attribute path.</summary>
public string Path { get; } = path ?? throw new ArgumentNullException(nameof(path));
/// <inheritdoc />
public override string ToString() => Path;
}

View file

@ -3,14 +3,27 @@
namespace Duende.Storage.Internal.Filtering.Expressions;
/// <summary>
/// Represents a comparison filter expression (e.g., attribute eq value).
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
/// <param name="attributePath">The attribute path being compared.</param>
/// <param name="op">The comparison operator.</param>
/// <param name="value">The value to compare against.</param>
public sealed class ComparisonExpression(AttributePathExpression attributePath, ComparisonOperator op, object? value)
: FilterExpression
{
/// <summary>Gets the attribute path being compared.</summary>
public AttributePathExpression AttributePath { get; } = attributePath ?? throw new ArgumentNullException(nameof(attributePath));
/// <summary>Gets the comparison operator.</summary>
public ComparisonOperator Operator { get; } = op;
/// <summary>Gets the comparison value.</summary>
public object? Value { get; } = value;
/// <inheritdoc />
public override string ToString() => $"{AttributePath} {Operator.ToFilterString()} {Value}";
}

View file

@ -3,14 +3,25 @@
namespace Duende.Storage.Internal.Filtering.Expressions;
/// <summary>
/// Represents a complex attribute filter expression with a nested filter (e.g., emails[type eq "work"]).
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
/// <param name="attributePath">The attribute path containing sub-attributes.</param>
/// <param name="filter">The nested filter expression applied to the complex attribute.</param>
public sealed class ComplexAttributeExpression(AttributePathExpression attributePath, FilterExpression filter)
: FilterExpression
{
/// <summary>Gets the attribute path.</summary>
public AttributePathExpression AttributePath { get; }
= attributePath ?? throw new ArgumentNullException(nameof(attributePath));
/// <summary>Gets the nested filter expression.</summary>
public FilterExpression Filter { get; }
= filter ?? throw new ArgumentNullException(nameof(filter));
/// <inheritdoc />
public override string ToString() => $"{AttributePath}[{Filter}]";
}

View file

@ -3,6 +3,12 @@
namespace Duende.Storage.Internal.Filtering.Expressions;
/// <summary>
/// Base class for all filter expression tree nodes.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public abstract class FilterExpression
{
}

View file

@ -3,14 +3,29 @@
namespace Duende.Storage.Internal.Filtering.Expressions;
/// <summary>
/// Represents a logical filter expression combining sub-expressions with AND, OR, or NOT.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed class LogicalExpression : FilterExpression
{
/// <summary>Gets the logical operator.</summary>
public LogicalOperator Operator { get; }
/// <summary>Gets the left (or only, for NOT) operand.</summary>
public FilterExpression Left { get; }
/// <summary>Gets the right operand, or null for NOT expressions.</summary>
public FilterExpression? Right { get; }
/// <summary>
/// Initializes a new instance of <see cref="LogicalExpression"/>.
/// </summary>
/// <param name="op">The logical operator.</param>
/// <param name="left">The left operand.</param>
/// <param name="right">The right operand (required for AND/OR, must be null for NOT).</param>
public LogicalExpression(LogicalOperator op, FilterExpression left, FilterExpression? right = null)
{
Operator = op;
@ -27,6 +42,7 @@ public sealed class LogicalExpression : FilterExpression
}
}
/// <inheritdoc />
public override string ToString() =>
Operator switch
{

View file

@ -6,8 +6,21 @@ using Duende.Storage.Querying;
namespace Duende.Storage.Internal.Filtering;
/// <summary>
/// Parses SCIM-style filter strings into a filter expression tree.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public static class FilterExpressionParser
{
/// <summary>
/// Parses a filter string into a <see cref="FilterExpression"/>.
/// </summary>
/// <param name="filter">The SCIM filter string to parse.</param>
/// <returns>The parsed filter expression tree.</returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="filter"/> is null or whitespace.</exception>
/// <exception cref="FilterParseException">Thrown when the filter string has invalid syntax.</exception>
public static FilterExpression Parse(string filter)
{
if (string.IsNullOrWhiteSpace(filter))
@ -36,6 +49,12 @@ public static class FilterExpressionParser
}
}
/// <summary>
/// Attempts to parse a filter string, returning a value indicating success.
/// </summary>
/// <param name="filter">The SCIM filter string to parse.</param>
/// <param name="expression">When successful, the parsed filter expression; otherwise, null.</param>
/// <returns><c>true</c> if parsing succeeded; otherwise, <c>false</c>.</returns>
public static bool TryParse(string filter, out FilterExpression? expression)
{
try

View file

@ -8,13 +8,28 @@ using Duende.Storage.Internal.Querying.Fields;
namespace Duende.Storage.Internal.Filtering;
/// <summary>
/// Translates parsed filter expression trees into query filter expressions.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed class FilterTranslator
{
private readonly IScimAttributeTypeResolver _resolver;
private readonly IQueryAttributeTypeResolver _resolver;
public FilterTranslator(IScimAttributeTypeResolver resolver) =>
/// <summary>
/// Initializes a new instance of <see cref="FilterTranslator"/>.
/// </summary>
/// <param name="resolver">The attribute type resolver for mapping attribute paths to fields.</param>
public FilterTranslator(IQueryAttributeTypeResolver resolver) =>
_resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
/// <summary>
/// Translates a filter string into a query filter expression.
/// </summary>
/// <param name="filter">The filter string to translate, or null for no filter.</param>
/// <returns>The query filter expression, or null if the filter is empty.</returns>
public IQueryFilterExpression? Translate(string? filter)
{
if (string.IsNullOrWhiteSpace(filter))
@ -26,6 +41,11 @@ public sealed class FilterTranslator
return Translate(expression);
}
/// <summary>
/// Translates a parsed filter expression into a query filter expression.
/// </summary>
/// <param name="expression">The filter expression to translate.</param>
/// <returns>The query filter expression.</returns>
public IQueryFilterExpression Translate(FilterExpression expression)
{
ArgumentNullException.ThrowIfNull(expression);
@ -148,7 +168,7 @@ public sealed class FilterTranslator
/// Inside an array filter, fields are per-element and the SQL builder
/// re-adds the array prefix when building the query.
/// </summary>
private sealed class ArrayElementResolver(IScimAttributeTypeResolver inner, string arrayPrefix) : IScimAttributeTypeResolver
private sealed class ArrayElementResolver(IQueryAttributeTypeResolver inner, string arrayPrefix) : IQueryAttributeTypeResolver
{
public Field ResolveField(string attributePath)
{

View file

@ -3,9 +3,20 @@
namespace Duende.Storage.Internal.Filtering;
/// <summary>
/// Defines the logical operators supported by the filter expression parser.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public enum LogicalOperator
{
/// <summary>Logical AND.</summary>
And,
/// <summary>Logical OR.</summary>
Or,
/// <summary>Logical NOT (negation).</summary>
Not
}

View file

@ -6,6 +6,9 @@ namespace Duende.Storage.Internal;
/// <summary>
/// Interface for DSK (Data Store Key) types.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public interface IDataStorageKey
{
/// <summary>

View file

@ -6,7 +6,13 @@ namespace Duende.Storage.Internal;
/// <summary>
/// Represents a Data Storage Object (DSO).
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public interface IDataStorageObject
{
/// <summary>
/// Gets the DSO version for this type.
/// </summary>
static abstract DataStorageObjectVersion DsoVersion { get; }
}

View file

@ -5,9 +5,15 @@ namespace Duende.Storage.Internal;
/// <summary>
/// Marks a dsk as only being a guid value. This means it won't be stored as serialized json
/// but only uses the guid value. IE: UserSubjectId, which is already a Guid.
/// but only uses the guid value. IE: UserSubjectId, which is already a Guid.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public interface IGuidDataStorageKey : IDataStorageKey
{
/// <summary>
/// Gets the GUID value of this key.
/// </summary>
Guid Value { get; }
}

View file

@ -3,7 +3,18 @@
namespace Duende.Storage.Internal;
/// <summary>
/// Represents a pooled store that supports multiple isolated pools sharing a single database.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public interface IPooledStore : IDatabaseSchema
{
/// <summary>
/// Opens a store scoped to the specified pool.
/// </summary>
/// <param name="poolId">The pool identifier.</param>
/// <returns>An <see cref="IStore"/> scoped to the specified pool.</returns>
IStore OpenPool(PoolId poolId);
}

View file

@ -6,15 +6,18 @@ using Duende.Storage.Internal.Querying.Fields;
namespace Duende.Storage.Internal;
/// <summary>
/// Resolves SCIM attribute paths to Faro Field types.
/// Resolves query attribute paths to Faro Field types.
/// Implementations define the schema-specific mapping for a resource type (User, Group, etc.).
/// </summary>
public interface IScimAttributeTypeResolver
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public interface IQueryAttributeTypeResolver
{
/// <summary>
/// Resolves a SCIM attribute path to its corresponding Faro Field.
/// Resolves a query attribute path to its corresponding Faro Field.
/// </summary>
/// <param name="attributePath">The SCIM attribute path (e.g., "userName", "name.familyName", "emails.value").</param>
/// <param name="attributePath">The attribute path (e.g., "userName", "name.familyName", "emails.value").</param>
/// <returns>The corresponding Faro Field instance.</returns>
/// <exception cref="NotSupportedException">Thrown when the attribute path is not recognized.</exception>
Field ResolveField(string attributePath);

View file

@ -13,6 +13,12 @@ using OutboxEventId = Duende.Storage.Internal.Outbox.OutboxEventId;
namespace Duende.Storage.Internal;
/// <summary>
/// Defines the core data store operations for entities, links, and outbox events.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public interface IStore
{
internal void SetPoolId(PoolId poolId);

View file

@ -17,10 +17,17 @@ namespace Duende.Storage.Internal;
/// <summary>
/// Decorates an <see cref="IStore"/> with tracing and metrics instrumentation.
/// </summary>
public sealed class InstrumentedStore(IStore inner, StorageMetrics metrics, string dbSystem) : IStore
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
internal sealed class InstrumentedStore(IStore inner, StorageMetrics metrics, string dbSystem) : IStore
{
/// <summary>
/// Gets the inner store being decorated.
/// </summary>
public IStore Inner => inner;
/// <inheritdoc />
public void SetPoolId(PoolId poolId) => inner.SetPoolId(poolId);
public async Task<CreateResult> CreateAsync<TDso>(

View file

@ -7,6 +7,9 @@ namespace Duende.Storage.Internal;
/// Defines the schema for a link type — binding a <see cref="LinkType"/> with its left and right <see cref="EntityType"/>s.
/// Define link definitions once as static instances and reference them everywhere.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record LinkDefinition
{
/// <summary>The entity type on the left side of the link.</summary>

View file

@ -6,6 +6,9 @@ namespace Duende.Storage.Internal;
/// <summary>
/// The result of a Link operation on <see cref="IStore"/>.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public enum LinkResult
{
/// <summary>The link was created successfully.</summary>

View file

@ -6,15 +6,30 @@ namespace Duende.Storage.Internal;
/// <summary>
/// The type of link between two entities.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
/// <param name="Id">A number representation for the link type.</param>
/// <param name="Name">The name of the link type. This name is only used for display purposes and should never change.</param>
public readonly record struct LinkType(uint Id, string Name)
{
/// <summary>
/// Obsolete parameterless constructor. Do not use.
/// </summary>
[Obsolete("Don't use this constructor")]
public LinkType() : this(0!, null!) => throw new InvalidOperationException("Cannot instantiate LinkType without parameters");
/// <summary>
/// Converts a <see cref="LinkTypeRegistry"/> value to a <see cref="LinkType"/>.
/// </summary>
/// <param name="registry">The registry value to convert.</param>
/// <returns>A <see cref="LinkType"/> instance.</returns>
public static LinkType ToLinkType(LinkTypeRegistry registry) =>
new((uint)registry, registry.ToString());
/// <summary>
/// Implicitly converts a <see cref="LinkTypeRegistry"/> value to a <see cref="LinkType"/>.
/// </summary>
/// <param name="value">The registry value to convert.</param>
public static implicit operator LinkType(LinkTypeRegistry value) => ToLinkType(value);
}

View file

@ -8,13 +8,26 @@ namespace Duende.Storage.Internal;
///
/// Once a link type is assigned an identifier, it must never be changed or reused for a different link type.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
#pragma warning disable CA1008 // Enums should have zero value. We explicitly want to avoid assigning a zero value to any link type, to catch uninitialized values.
public enum LinkTypeRegistry
#pragma warning restore CA1008
{
/// <summary>
/// Link type for group-to-role relationships.
/// </summary>
// user profile link types (1500-1599 range, aligned with entity types)
GroupRole = 1502,
/// <summary>
/// Link type for membership-to-role relationships.
/// </summary>
MembershipRole = 1503,
/// <summary>
/// Link type for membership-to-group relationships.
/// </summary>
MembershipGroup = 1504,
}

View file

@ -6,6 +6,9 @@ namespace Duende.Storage.Internal.Operations;
/// <summary>
/// Represents the result of a batch operation.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
/// <param name="Success">True if all operations succeeded; false if any failed (and all were rolled back).</param>
/// <param name="Results">The outcome of each operation, in order.</param>
public sealed record BatchResult(bool Success, IReadOnlyList<OperationResult> Results)

View file

@ -8,6 +8,9 @@ namespace Duende.Storage.Internal.Operations;
/// <summary>
/// Represents a create operation for batch processing.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed class CreateOperation : IStoreOperation
{
private CreateOperation(

View file

@ -3,10 +3,31 @@
namespace Duende.Storage.Internal.Operations;
/// <summary>
/// Represents the possible outcomes of a create operation.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public enum CreateResult
{
/// <summary>
/// The entity was created successfully.
/// </summary>
Success,
/// <summary>
/// The entity already exists with the same identifier.
/// </summary>
AlreadyExists,
/// <summary>
/// A key conflict occurred with another entity.
/// </summary>
KeyConflict,
/// <summary>
/// A concurrency conflict occurred during creation.
/// </summary>
ConcurrencyConflict
}

View file

@ -6,6 +6,9 @@ namespace Duende.Storage.Internal.Operations;
/// <summary>
/// Represents a delete operation for batch processing.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed class DeleteOperation : IStoreOperation
{
private DeleteOperation(EntityType entityType, UuidV7? id, DataStorageKey? key)

View file

@ -3,7 +3,16 @@
namespace Duende.Storage.Internal.Operations;
/// <summary>
/// Represents the possible outcomes of a delete operation.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public enum DeleteResult
{
/// <summary>
/// The entity was deleted successfully.
/// </summary>
Success
}

View file

@ -6,6 +6,9 @@ namespace Duende.Storage.Internal.Operations;
/// <summary>
/// Marker interface for batch operations.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public interface IStoreOperation
{
/// <summary>

View file

@ -7,6 +7,9 @@ namespace Duende.Storage.Internal.Operations;
/// Represents a link operation for batch processing.
/// Creates a link between two entities as part of an atomic batch.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed class LinkOperation : IStoreOperation
{
private LinkOperation(LinkDefinition definition, UuidV7 leftEntityId, UuidV7 rightEntityId)

View file

@ -6,6 +6,9 @@ namespace Duende.Storage.Internal.Operations;
/// <summary>
/// Represents the outcome of an individual operation within a batch.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public enum OperationOutcome
{
/// <summary>

View file

@ -6,6 +6,9 @@ namespace Duende.Storage.Internal.Operations;
/// <summary>
/// Represents the result of an individual operation within a batch.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
/// <param name="Index">The zero-based index of the operation in the batch.</param>
/// <param name="Outcome">The outcome of the operation.</param>
public sealed record OperationResult(int Index, OperationOutcome Outcome);

View file

@ -8,8 +8,19 @@ namespace Duende.Storage.Internal.Operations;
/// <summary>
/// Wraps the result of a Get operation from the store.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record StoreGetResult
{
/// <summary>
/// Initializes a new instance of the <see cref="StoreGetResult"/> class representing a found entity.
/// </summary>
/// <param name="dso">The data storage object.</param>
/// <param name="id">The unique identifier of the entity.</param>
/// <param name="version">The version of the entity.</param>
/// <param name="createdAt">The creation timestamp.</param>
/// <param name="lastUpdatedAt">The last update timestamp.</param>
public StoreGetResult(IDataStorageObject dso, Guid id, int version, DateTimeOffset createdAt, DateTimeOffset lastUpdatedAt)
{
Found = true;
@ -24,23 +35,54 @@ public sealed record StoreGetResult
{
}
/// <summary>
/// Creates a result indicating the entity was not found.
/// </summary>
/// <returns>A <see cref="StoreGetResult"/> representing a not-found result.</returns>
public static StoreGetResult NotFound() => new();
/// <summary>
/// Gets a value indicating whether the entity was found.
/// </summary>
[MemberNotNullWhen(true, nameof(Dso))]
[MemberNotNullWhen(true, nameof(Id))]
[MemberNotNullWhen(true, nameof(Version))]
public bool Found { get; }
/// <summary>
/// Gets the data storage object, or <c>null</c> if not found.
/// </summary>
public IDataStorageObject? Dso { get; }
/// <summary>
/// Gets the unique identifier, or <c>null</c> if not found.
/// </summary>
public Guid? Id { get; }
/// <summary>
/// Gets the entity version, or <c>null</c> if not found.
/// </summary>
public int? Version { get; }
/// <summary>
/// Gets the creation timestamp.
/// </summary>
public DateTimeOffset CreatedAt { get; }
/// <summary>
/// Gets the last update timestamp.
/// </summary>
public DateTimeOffset LastUpdatedAt { get; }
/// <summary>
/// Creates a result indicating the entity was found.
/// </summary>
/// <param name="item">The data storage object.</param>
/// <param name="id">The unique identifier.</param>
/// <param name="version">The entity version.</param>
/// <param name="createdAt">The creation timestamp.</param>
/// <param name="lastUpdatedAt">The last update timestamp.</param>
/// <returns>A <see cref="StoreGetResult"/> representing a found result.</returns>
public static StoreGetResult IsFound(IDataStorageObject item, Guid id, int version, DateTimeOffset createdAt, DateTimeOffset lastUpdatedAt) =>
new(item, id, version, createdAt, lastUpdatedAt);
}

View file

@ -7,6 +7,9 @@ namespace Duende.Storage.Internal.Operations;
/// Represents an unlink operation for batch processing.
/// Removes a link between two entities as part of an atomic batch.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed class UnlinkOperation : IStoreOperation
{
private UnlinkOperation(LinkDefinition definition, UuidV7 leftEntityId, UuidV7 rightEntityId)

View file

@ -8,6 +8,9 @@ namespace Duende.Storage.Internal.Operations;
/// <summary>
/// Represents an update operation for batch processing.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed class UpdateOperation : IStoreOperation
{
private UpdateOperation(

View file

@ -3,10 +3,31 @@
namespace Duende.Storage.Internal.Operations;
/// <summary>
/// Represents the possible outcomes of an update operation.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public enum UpdateResult
{
/// <summary>
/// The entity was updated successfully.
/// </summary>
Success,
/// <summary>
/// The entity does not exist.
/// </summary>
DoesNotExist,
/// <summary>
/// The entity version did not match the expected version.
/// </summary>
UnexpectedVersion,
/// <summary>
/// A key conflict occurred with another entity.
/// </summary>
KeyConflict
}

View file

@ -7,6 +7,9 @@ namespace Duende.Storage.Internal.Outbox;
/// Represents the outcome of a handler processing an outbox event.
/// Handlers return one of: <see cref="Success"/>, <see cref="Retry"/>, or <see cref="Drop"/>.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public abstract record HandleOutcomeResult
{
private HandleOutcomeResult() { }

View file

@ -7,6 +7,9 @@ namespace Duende.Storage.Internal.Outbox;
/// Declares a subscriber that wants to receive outbox events via the fanout mechanism.
/// Subscribers are registered in DI and matched against outbox events by event name and entity type.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public interface IOutboxSubscriber
{
/// <summary>The unique name identifying this subscriber.</summary>

View file

@ -7,6 +7,9 @@ namespace Duende.Storage.Internal.Outbox;
/// Defines the handler contract for processing outbox events for a specific subscriber.
/// Implementations are registered in DI using keyed services, keyed by subscriber name.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public interface IOutboxSubscriberHandler
{
/// <summary>Handles a single outbox event delivered to this subscriber.</summary>

View file

@ -8,6 +8,9 @@ namespace Duende.Storage.Internal.Outbox;
/// The store implementation stamps <c>PoolId</c> from the ambient pool context, and the
/// database automatically assigns the <c>SequenceNumber</c> on insert.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record OutboxEvent
{
/// <summary>The unique identifier for this outbox event.</summary>

View file

@ -3,6 +3,12 @@
namespace Duende.Storage.Internal.Outbox;
/// <summary>
/// Represents a unique identifier for an outbox event.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
[ValueOf<Guid>]
public partial record OutboxEventId
{

View file

@ -8,6 +8,9 @@ namespace Duende.Storage.Internal.Outbox;
/// <summary>
/// The name of a domain event written to the outbox. Only alphanumeric characters, underscores, and hyphens are allowed.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
[StringValue]
public partial record OutboxEventName
{

View file

@ -6,6 +6,9 @@ namespace Duende.Storage.Internal.Outbox;
/// <summary>
/// A paged result of outbox events ordered by sequence number.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
/// <param name="Events">The outbox events in this page.</param>
/// <param name="HasMore">Whether there are more events beyond this page.</param>
public sealed record OutboxEventsPage(IReadOnlyList<PersistedOutboxEvent> Events, bool HasMore);

View file

@ -7,6 +7,9 @@ namespace Duende.Storage.Internal.Outbox;
/// Represents an outbox event as read from the database, including the
/// database-assigned sequence number and the store-stamped space identifier.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record PersistedOutboxEvent
{
/// <summary>The store-generated unique identifier for this persisted message (one per subscriber fanout row).</summary>

View file

@ -8,6 +8,9 @@ namespace Duende.Storage.Internal.Outbox;
/// <summary>
/// The unique name identifying an outbox subscriber. Only alphanumeric characters, underscores, and hyphens are allowed.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
[StringValue]
public partial record SubscriberName
{

View file

@ -3,5 +3,11 @@
namespace Duende.Storage.Internal;
/// <summary>
/// Represents a pool identifier.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
[ValueOf<int>]
public partial record PoolId;

View file

@ -8,6 +8,9 @@ namespace Duende.Storage.Internal;
/// When used, the query returns <see cref="EntityAttributeValue.AttributeValueCollection"/>
/// instead of a fully-typed DTO.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record Projection
{
/// <summary>The attribute names to include.</summary>

View file

@ -9,7 +9,10 @@ namespace Duende.Storage.Internal.Querying;
/// <summary>
/// Represents a decoded cursor token containing the last-seen position for seek-based pagination.
/// </summary>
public sealed record CursorToken
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
internal sealed record CursorToken
{
/// <summary>
/// The ID of the last entity on the previous page.

View file

@ -6,6 +6,9 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// <summary>
/// Singleton expression representing 'match all records' - no filter applied.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record AllExpression : IQueryFilterExpression
{
/// <summary>

View file

@ -7,6 +7,9 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// Expression that combines multiple filter expressions with AND logic.
/// All conditions must be true for the expression to match.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record AndExpression : IQueryFilterExpression
{
/// <summary>
@ -14,6 +17,10 @@ public sealed record AndExpression : IQueryFilterExpression
/// </summary>
public IReadOnlyList<IQueryFilterExpression> Parts { get; }
/// <summary>
/// Initializes a new instance of <see cref="AndExpression"/> with the specified parts.
/// </summary>
/// <param name="parts">The filter expressions to combine with AND logic.</param>
public AndExpression(IReadOnlyList<IQueryFilterExpression> parts)
{
ArgumentNullException.ThrowIfNull(parts);
@ -26,6 +33,10 @@ public sealed record AndExpression : IQueryFilterExpression
Parts = parts;
}
/// <summary>
/// Initializes a new instance of <see cref="AndExpression"/> with the specified parts.
/// </summary>
/// <param name="parts">The filter expressions to combine with AND logic.</param>
public AndExpression(params IQueryFilterExpression[] parts)
: this((IReadOnlyList<IQueryFilterExpression>)parts)
{

View file

@ -9,4 +9,7 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// Expression that checks if a string array field contains an element equal to the specified value.
/// Generates an EXISTS subquery with item_index >= 0 to match any array element.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record ArrayContainsExpression(StringArrayField Field, string Value) : IQueryExpression, IQueryFilterExpression;

View file

@ -10,6 +10,9 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// Example: emails[type eq "work" and value co "@example.com"]
/// This ensures both conditions match the same email in the emails array.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record ArrayFilterExpression : IQueryFilterExpression
{
/// <summary>
@ -23,6 +26,11 @@ public sealed record ArrayFilterExpression : IQueryFilterExpression
/// </summary>
public IQueryFilterExpression Filter { get; }
/// <summary>
/// Initializes a new instance of <see cref="ArrayFilterExpression"/>.
/// </summary>
/// <param name="arrayFieldPath">The array field path.</param>
/// <param name="filter">The filter expression to apply within each array element.</param>
public ArrayFilterExpression(string arrayFieldPath, IQueryFilterExpression filter)
{
if (string.IsNullOrWhiteSpace(arrayFieldPath))

View file

@ -8,12 +8,26 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// <summary>
/// Expression that checks if a field value is between two values (inclusive).
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record BetweenExpression : IQueryFilterExpression
{
/// <summary>Gets the field to compare.</summary>
public Field Field { get; }
/// <summary>Gets the minimum value (inclusive).</summary>
public object Min { get; }
/// <summary>Gets the maximum value (inclusive).</summary>
public object Max { get; }
/// <summary>
/// Initializes a new instance of <see cref="BetweenExpression"/>.
/// </summary>
/// <param name="field">The field to compare.</param>
/// <param name="min">The minimum value (inclusive).</param>
/// <param name="max">The maximum value (inclusive).</param>
public BetweenExpression(Field field, object min, object max)
{
Field = field ?? throw new ArgumentNullException(nameof(field));

View file

@ -8,11 +8,22 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// <summary>
/// Expression that checks if a string field contains a specified substring.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record ContainsExpression : IQueryFilterExpression
{
/// <summary>Gets the string field to search.</summary>
public StringField Field { get; }
/// <summary>Gets the substring to search for.</summary>
public string Value { get; }
/// <summary>
/// Initializes a new instance of <see cref="ContainsExpression"/>.
/// </summary>
/// <param name="field">The string field to search.</param>
/// <param name="value">The substring to search for.</param>
public ContainsExpression(StringField field, string value)
{
Field = field ?? throw new ArgumentNullException(nameof(field));

View file

@ -9,11 +9,22 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// Expression that checks if a string field ends with a specified suffix.
/// Used for the SCIM 'ew' (ends with) operator.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record EndsWithExpression : IQueryFilterExpression
{
/// <summary>Gets the string field to check.</summary>
public StringField Field { get; }
/// <summary>Gets the suffix value.</summary>
public string Value { get; }
/// <summary>
/// Initializes a new instance of <see cref="EndsWithExpression"/>.
/// </summary>
/// <param name="field">The string field to check.</param>
/// <param name="value">The suffix to match.</param>
public EndsWithExpression(StringField field, string value)
{
Field = field ?? throw new ArgumentNullException(nameof(field));

View file

@ -8,11 +8,22 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// <summary>
/// Expression that checks if a field equals a specified value.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record EqualExpression : IQueryFilterExpression
{
/// <summary>Gets the field to compare.</summary>
public Field Field { get; }
/// <summary>Gets the value to compare against.</summary>
public object Value { get; }
/// <summary>
/// Initializes a new instance of <see cref="EqualExpression"/>.
/// </summary>
/// <param name="field">The field to compare.</param>
/// <param name="value">The value to compare against.</param>
public EqualExpression(Field field, object value)
{
Field = field ?? throw new ArgumentNullException(nameof(field));

View file

@ -8,11 +8,22 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// <summary>
/// Expression that checks if a field is greater than or equal to a specified value.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record GreaterOrEqualExpression : IQueryFilterExpression
{
/// <summary>Gets the field to compare.</summary>
public Field Field { get; }
/// <summary>Gets the value to compare against.</summary>
public object Value { get; }
/// <summary>
/// Initializes a new instance of <see cref="GreaterOrEqualExpression"/>.
/// </summary>
/// <param name="field">The field to compare.</param>
/// <param name="value">The value to compare against.</param>
public GreaterOrEqualExpression(Field field, object value)
{
Field = field ?? throw new ArgumentNullException(nameof(field));

View file

@ -8,11 +8,22 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// <summary>
/// Expression that checks if a field is greater than a specified value.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record GreaterThanExpression : IQueryFilterExpression
{
/// <summary>Gets the field to compare.</summary>
public Field Field { get; }
/// <summary>Gets the value to compare against.</summary>
public object Value { get; }
/// <summary>
/// Initializes a new instance of <see cref="GreaterThanExpression"/>.
/// </summary>
/// <param name="field">The field to compare.</param>
/// <param name="value">The value to compare against.</param>
public GreaterThanExpression(Field field, object value)
{
Field = field ?? throw new ArgumentNullException(nameof(field));

View file

@ -9,11 +9,22 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// <summary>
/// Expression that checks if a field value is in a specified collection.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record InExpression : IQueryFilterExpression
{
/// <summary>Gets the field to check.</summary>
public Field Field { get; }
/// <summary>Gets the collection of values to match against.</summary>
public IEnumerable Values { get; }
/// <summary>
/// Initializes a new instance of <see cref="InExpression"/>.
/// </summary>
/// <param name="field">The field to check.</param>
/// <param name="values">The collection of values.</param>
public InExpression(Field field, IEnumerable values)
{
Field = field ?? throw new ArgumentNullException(nameof(field));

View file

@ -8,11 +8,22 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// <summary>
/// Expression that checks if a field is less than or equal to a specified value.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record LessOrEqualExpression : IQueryFilterExpression
{
/// <summary>Gets the field to compare.</summary>
public Field Field { get; }
/// <summary>Gets the value to compare against.</summary>
public object Value { get; }
/// <summary>
/// Initializes a new instance of <see cref="LessOrEqualExpression"/>.
/// </summary>
/// <param name="field">The field to compare.</param>
/// <param name="value">The value to compare against.</param>
public LessOrEqualExpression(Field field, object value)
{
Field = field ?? throw new ArgumentNullException(nameof(field));

View file

@ -8,11 +8,22 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// <summary>
/// Expression that checks if a field is less than a specified value.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record LessThanExpression : IQueryFilterExpression
{
/// <summary>Gets the field to compare.</summary>
public Field Field { get; }
/// <summary>Gets the value to compare against.</summary>
public object Value { get; }
/// <summary>
/// Initializes a new instance of <see cref="LessThanExpression"/>.
/// </summary>
/// <param name="field">The field to compare.</param>
/// <param name="value">The value to compare against.</param>
public LessThanExpression(Field field, object value)
{
Field = field ?? throw new ArgumentNullException(nameof(field));

View file

@ -7,6 +7,9 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// Expression that negates an inner filter expression.
/// Used for SCIM 'not' operator and 'ne' (as Not(Equal(...))).
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record NotExpression : IQueryFilterExpression
{
/// <summary>
@ -14,6 +17,10 @@ public sealed record NotExpression : IQueryFilterExpression
/// </summary>
public IQueryFilterExpression Inner { get; }
/// <summary>
/// Initializes a new instance of <see cref="NotExpression"/>.
/// </summary>
/// <param name="inner">The expression to negate.</param>
public NotExpression(IQueryFilterExpression inner) =>
Inner = inner ?? throw new ArgumentNullException(nameof(inner));
}

View file

@ -7,6 +7,9 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// Expression that combines multiple filter expressions with OR logic.
/// At least one condition must be true for the expression to match.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record OrExpression : IQueryFilterExpression
{
/// <summary>
@ -14,6 +17,10 @@ public sealed record OrExpression : IQueryFilterExpression
/// </summary>
public IReadOnlyList<IQueryFilterExpression> Parts { get; }
/// <summary>
/// Initializes a new instance of <see cref="OrExpression"/> with the specified parts.
/// </summary>
/// <param name="parts">The filter expressions to combine with OR logic.</param>
public OrExpression(IReadOnlyList<IQueryFilterExpression> parts)
{
ArgumentNullException.ThrowIfNull(parts);
@ -26,6 +33,10 @@ public sealed record OrExpression : IQueryFilterExpression
Parts = parts;
}
/// <summary>
/// Initializes a new instance of <see cref="OrExpression"/> with the specified parts.
/// </summary>
/// <param name="parts">The filter expressions to combine with OR logic.</param>
public OrExpression(params IQueryFilterExpression[] parts)
: this((IReadOnlyList<IQueryFilterExpression>)parts)
{

View file

@ -11,10 +11,18 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// For scalar fields, checks that a search_values row exists with a non-null typed column.
/// For array fields, checks that at least one row with item_index >= 0 exists.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record PresentExpression : IQueryFilterExpression
{
/// <summary>Gets the field to check for presence.</summary>
public Field Field { get; }
/// <summary>
/// Initializes a new instance of <see cref="PresentExpression"/>.
/// </summary>
/// <param name="field">The field to check for presence.</param>
public PresentExpression(Field field) =>
Field = field ?? throw new ArgumentNullException(nameof(field));
}

View file

@ -8,11 +8,22 @@ namespace Duende.Storage.Internal.Querying.Expressions;
/// <summary>
/// Expression that checks if a string field starts with a specified value.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record StartsWithExpression : IQueryFilterExpression
{
/// <summary>Gets the string field to check.</summary>
public StringField Field { get; }
/// <summary>Gets the prefix value.</summary>
public string Value { get; }
/// <summary>
/// Initializes a new instance of <see cref="StartsWithExpression"/>.
/// </summary>
/// <param name="field">The string field to check.</param>
/// <param name="value">The prefix to match.</param>
public StartsWithExpression(StringField field, string value)
{
Field = field ?? throw new ArgumentNullException(nameof(field));

View file

@ -8,8 +8,16 @@ namespace Duende.Storage.Internal.Querying.Fields;
/// <summary>
/// Represents a boolean-valued field with boolean-specific operations.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record BooleanField : Field
{
/// <summary>
/// Initializes a new instance of <see cref="BooleanField"/>.
/// </summary>
/// <param name="path">The field path.</param>
/// <param name="isMultiValued">Whether the field is multi-valued.</param>
public BooleanField(string path, bool isMultiValued = false) : base(path, FieldType.Boolean, isMultiValued)
{
}

View file

@ -8,8 +8,16 @@ namespace Duende.Storage.Internal.Querying.Fields;
/// <summary>
/// Represents a DateTimeOffset-valued field with temporal comparison operations.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record DateTimeField : Field
{
/// <summary>
/// Initializes a new instance of <see cref="DateTimeField"/>.
/// </summary>
/// <param name="path">The field path.</param>
/// <param name="isMultiValued">Whether the field is multi-valued.</param>
public DateTimeField(string path, bool isMultiValued = false) : base(path, FieldType.DateTime, isMultiValued)
{
}

View file

@ -11,8 +11,15 @@ namespace Duende.Storage.Internal.Querying.Fields;
/// Queries use the guid_value column with hashed values for fast lookups.
/// No string_value is stored — only the deterministic hash in guid_value.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public sealed record ExactMatchField : Field
{
/// <summary>
/// Initializes a new instance of <see cref="ExactMatchField"/>.
/// </summary>
/// <param name="path">The field path.</param>
public ExactMatchField(string path) : base(path, FieldType.Guid)
{
}

View file

@ -8,6 +8,9 @@ namespace Duende.Storage.Internal.Querying.Fields;
/// <summary>
/// Base class for all field types, representing a queryable field path.
/// </summary>
/// <remarks>
/// This type is for usage by Duende Software products, is not supported for end user consumption, and not subject to semantic versioning rules.
/// </remarks>
public abstract record Field
{
/// <summary>
@ -28,6 +31,12 @@ public abstract record Field
/// </summary>
public bool IsMultiValued { get; }
/// <summary>
/// Initializes a new instance of <see cref="Field"/>.
/// </summary>
/// <param name="path">The field path.</param>
/// <param name="type">The field type.</param>
/// <param name="isMultiValued">Whether the field is multi-valued.</param>
protected Field(string path, FieldType type, bool isMultiValued = false)
{
if (string.IsNullOrWhiteSpace(path))

Some files were not shown because too many files have changed in this diff Show more