diff --git a/src/Gemstone.Data/Model/ExpressionTableOperations.cs b/src/Gemstone.Data/Model/ExpressionTableOperations.cs
new file mode 100644
index 000000000..cc07c2f49
--- /dev/null
+++ b/src/Gemstone.Data/Model/ExpressionTableOperations.cs
@@ -0,0 +1,146 @@
+//******************************************************************************************************
+// ExpressionTableOperations.cs - Gbtc
+//
+// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
+//
+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
+// file except in compliance with the License. You may obtain a copy of the License at:
+//
+// http://opensource.org/licenses/MIT
+//
+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
+// License for the specific language governing permissions and limitations.
+//
+// Code Modification History:
+// ----------------------------------------------------------------------------------------------------
+// 06/25/2026 - J. Ritchie Carroll
+// Generated original version of source code.
+//
+//******************************************************************************************************
+// ReSharper disable StaticMemberInGenericType
+
+using System;
+using System.Collections.Generic;
+using Gemstone.Expressions.Evaluator;
+using Gemstone.Expressions.Model;
+
+namespace Gemstone.Data.Model;
+
+///
+/// Defines database operations for a modeled table that additionally evaluates attribute-based value
+/// expressions, i.e., and
+/// , when creating, defaulting and updating records.
+///
+/// Modeled table.
+///
+///
+/// The base is a pure POCO operator: it honors only the standard
+/// and carries no dependency on the
+/// . This keeps the simple case working without
+/// any external setup, e.g., new TableOperations<MyModel>(connection).NewRecord() will never
+/// fail because of an unconfigured value-expression dependency such as Settings.
+///
+///
+/// Use to opt in to the richer value-expression behavior. When
+/// using this type, the caller is responsible for ensuring any dependencies referenced by the model's
+/// value expressions (e.g., Settings, UserInfo, custom symbols registered via the
+/// ) are properly initialized.
+///
+///
+public class ExpressionTableOperations : TableOperations where T : class, new()
+{
+ // Nested Types
+ private class CurrentScope : ValueExpressionScopeBase
+ {
+ // Define instance variables exposed to ValueExpressionAttributeBase expressions
+ #pragma warning disable 169, 414, 649, CS8618
+ public TableOperations TableOperations;
+ public AdoDataConnection Connection;
+ #pragma warning restore 169, 414, 649, CS8618
+ }
+
+ ///
+ /// Creates a new instance.
+ ///
+ /// instance to use for database operations.
+ /// Custom run-time tokens to apply to any modeled values.
+ public ExpressionTableOperations(AdoDataConnection connection, IEnumerable>? customTokens = null)
+ : base(connection, customTokens)
+ {
+ }
+
+ ///
+ /// Creates a new instance.
+ ///
+ /// instance to use for database operations.
+ /// Delegate to handle table operation exceptions.
+ /// Custom run-time tokens to apply to any modeled values.
+ ///
+ /// When exception handler is provided, table operations will not throw exceptions for database calls, any
+ /// encountered exceptions will be passed to handler for processing. Otherwise, exceptions will be thrown
+ /// on the call stack.
+ ///
+ public ExpressionTableOperations(AdoDataConnection connection, Action exceptionHandler, IEnumerable>? customTokens = null)
+ : base(connection, exceptionHandler, customTokens)
+ {
+ }
+
+ ///
+ protected override T CreateRecordInstance()
+ {
+ return s_createRecordInstance(new CurrentScope { TableOperations = this, Connection = Connection });
+ }
+
+ ///
+ protected override void ApplyRecordDefaultValues(T record)
+ {
+ s_applyRecordDefaults(new CurrentScope { Instance = record, TableOperations = this, Connection = Connection });
+ }
+
+ ///
+ protected override void ApplyRecordUpdateValues(T record)
+ {
+ s_updateRecordInstance(new CurrentScope { Instance = record, TableOperations = this, Connection = Connection });
+ }
+
+ // Static Fields
+ private static readonly Func s_createRecordInstance;
+ private static readonly Action s_updateRecordInstance;
+ private static readonly Action s_applyRecordDefaults;
+ private static TypeRegistry? s_typeRegistry;
+
+ // Static Constructor
+ static ExpressionTableOperations()
+ {
+ // Create an instance of modeled table to allow any static functionality to be initialized,
+ // such as registering any custom types or symbols that may be useful for value expressions
+ ValueExpressionParser.InitializeType();
+
+ // Generate compiled "create new", "apply defaults" and "update" record functions for modeled table,
+ // honoring DefaultValueAttribute, DefaultValueExpressionAttribute and UpdateValueExpressionAttribute.
+ // Note: RecordProperties is sourced from the base table operations for the same modeled type T.
+ s_createRecordInstance = ValueExpressionParser.CreateInstance(RecordProperties, s_typeRegistry);
+ s_updateRecordInstance = ValueExpressionParser.UpdateInstance(RecordProperties, s_typeRegistry);
+ s_applyRecordDefaults = ValueExpressionParser.ApplyDefaults(RecordProperties, s_typeRegistry);
+ }
+
+ // Static Properties
+
+ ///
+ /// Gets or sets instance used for evaluating encountered instances
+ /// of the on modeled table properties.
+ ///
+ ///
+ /// Accessing this property will create a unique type registry for the current type which
+ /// will initially contain the values found in the
+ /// and can be augmented with custom types. Set to null to restore use of the default type registry.
+ ///
+ public static TypeRegistry TypeRegistry
+ {
+ get => s_typeRegistry ??= ValueExpressionParser.DefaultTypeRegistry.Clone();
+ set => s_typeRegistry = value;
+ }
+}
diff --git a/src/Gemstone.Data/Model/SecureTableOperations.cs b/src/Gemstone.Data/Model/SecureTableOperations.cs
index 091bda3aa..b3ee1a5de 100644
--- a/src/Gemstone.Data/Model/SecureTableOperations.cs
+++ b/src/Gemstone.Data/Model/SecureTableOperations.cs
@@ -57,7 +57,7 @@ public SecureTableOperations(TableOperations operations)
/// db to create secure operations to.
public SecureTableOperations(AdoDataConnection connection)
{
- BaseOperations = new(connection);
+ BaseOperations = new ExpressionTableOperations(connection);
}
#region [ Properties ]
diff --git a/src/Gemstone.Data/Model/TableOperations.cs b/src/Gemstone.Data/Model/TableOperations.cs
index 9390ce344..97c6b62f8 100644
--- a/src/Gemstone.Data/Model/TableOperations.cs
+++ b/src/Gemstone.Data/Model/TableOperations.cs
@@ -38,6 +38,7 @@
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
@@ -48,11 +49,11 @@
using Gemstone.Collections.IAsyncEnumerableExtensions;
using Gemstone.Data.DataExtensions;
using Gemstone.Diagnostics;
-using Gemstone.Expressions.Evaluator;
using Gemstone.Expressions.Model;
using Gemstone.Reflection.MemberInfoExtensions;
using Gemstone.Security.Cryptography;
using Gemstone.StringExtensions;
+using LinqExpression = System.Linq.Expressions.Expression;
namespace Gemstone.Data.Model;
@@ -65,15 +66,6 @@ namespace Gemstone.Data.Model;
#region [ Members ]
// Nested Types
- private class CurrentScope : ValueExpressionScopeBase
- {
- // Define instance variables exposed to ValueExpressionAttributeBase expressions
- #pragma warning disable 169, 414, 649, CS8618
- public TableOperations TableOperations;
- public AdoDataConnection Connection;
- #pragma warning restore 169, 414, 649, CS8618
- }
-
private class NullConnection : DbConnection
{
[AllowNull]
@@ -407,17 +399,66 @@ public TableOperations(AdoDataConnection connection, Action exception
#region [ Methods ]
+ ///
+ /// Creates a new modeled record instance and applies any modeled default values.
+ ///
+ /// New modeled record instance with any defined default values applied.
+ ///
+ /// The base implementation creates a new
+ /// instance and applies only the constant values specified by any
+ /// on the model properties. Derived types, such as
+ /// , may override this method to apply additional behavior,
+ /// e.g., evaluation of instances.
+ ///
+ protected virtual T CreateRecordInstance()
+ {
+ T record = new();
+ s_applyDefaultValues(record);
+ return record;
+ }
+
+ ///
+ /// Applies any modeled default values to the specified .
+ ///
+ /// Record to update.
+ ///
+ /// The base implementation applies only the constant values specified
+ /// by any on the model properties. Derived types may override this
+ /// method to apply additional behavior, e.g., evaluation of
+ /// instances.
+ ///
+ protected virtual void ApplyRecordDefaultValues(T record)
+ {
+ s_applyDefaultValues(record);
+ }
+
+ ///
+ /// Applies any modeled update values to the specified .
+ ///
+ /// Record to update.
+ ///
+ /// The base implementation performs no action since update value
+ /// expressions are not part of standard POCO behavior. Derived types may override this method to apply
+ /// additional behavior, e.g., evaluation of instances.
+ ///
+ protected virtual void ApplyRecordUpdateValues(T record)
+ {
+ }
+
///
/// Creates a new modeled record instance, applying any modeled default values as specified by a
- /// or on the
- /// model properties.
+ /// on the model properties.
///
/// New modeled record instance with any defined default values applied.
+ ///
+ /// To also apply values, use
+ /// .
+ ///
public T? NewRecord()
{
try
{
- return s_createRecordInstance(new CurrentScope { TableOperations = this, Connection = Connection });
+ return CreateRecordInstance();
}
catch (Exception ex)
{
@@ -437,15 +478,18 @@ public TableOperations(AdoDataConnection connection, Action exception
///
/// Applies the default values on the specified modeled table
- /// where any of the properties are marked with either
- /// or .
+ /// where any of the properties are marked with a .
///
/// Record to update.
+ ///
+ /// To also apply values, use
+ /// .
+ ///
public void ApplyRecordDefaults(T record)
{
try
{
- s_applyRecordDefaults(new CurrentScope { Instance = record, TableOperations = this, Connection = Connection });
+ ApplyRecordDefaultValues(record);
}
catch (Exception ex)
{
@@ -465,15 +509,18 @@ void ITableOperations.ApplyRecordDefaults(object value)
}
///
- /// Applies the update values on the specified modeled table where
- /// any of the properties are marked with .
+ /// Applies the update values on the specified modeled table .
///
/// Record to update.
+ ///
+ /// The base implementation performs no action. To apply
+ /// values, use .
+ ///
public void ApplyRecordUpdates(T record)
{
try
{
- s_updateRecordInstance(new CurrentScope { Instance = record, TableOperations = this, Connection = Connection });
+ ApplyRecordUpdateValues(record);
}
catch (Exception ex)
{
@@ -2136,7 +2183,7 @@ private TReturn UpdateRecordOperation(T record, TReturn zeroReturn, Fun
try
{
- s_updateRecordInstance(new CurrentScope { Instance = record, TableOperations = this, Connection = Connection });
+ ApplyRecordUpdateValues(record);
if (RootQueryRestriction is not null && (applyRootQueryRestriction ?? ApplyRootQueryRestrictionToUpdates))
restriction = (RootQueryRestriction + restriction)!;
@@ -2986,16 +3033,13 @@ private bool FieldIsEncrypted(string fieldName)
private static readonly string s_deleteWhereSql;
private static readonly string s_primaryKeyFields;
private static readonly bool s_hasPrimaryKeyIdentityField;
- private static readonly Func s_createRecordInstance;
- private static readonly Action s_updateRecordInstance;
- private static readonly Action s_applyRecordDefaults;
+ private static readonly Action s_applyDefaultValues;
private static readonly DataTable s_tableSchema;
private static readonly HashSet s_validFieldNames;
private static readonly HashSet s_searchableFields;
private static readonly (Regex, Func)[] s_searchExtensions;
private static readonly (Regex, Func)[] s_sortExtensions;
private static readonly HashSet s_excludedFields;
- private static TypeRegistry? s_typeRegistry;
// Static Constructor
static TableOperations()
@@ -3168,14 +3212,11 @@ static TableOperations()
s_updateProperties = updateProperties.ToArray();
s_primaryKeyProperties = primaryKeyProperties.ToArray();
- // Create an instance of modeled table to allow any static functionality to be initialized,
- // such as registering any custom types or symbols that may be useful for value expressions
- ValueExpressionParser.InitializeType();
-
- // Generate compiled "create new" and "update" record functions for modeled table
- s_createRecordInstance = ValueExpressionParser.CreateInstance(s_properties.Values, s_typeRegistry);
- s_updateRecordInstance = ValueExpressionParser.UpdateInstance(s_properties.Values, s_typeRegistry);
- s_applyRecordDefaults = ValueExpressionParser.ApplyDefaults(s_properties.Values, s_typeRegistry);
+ // Generate compiled function that applies any modeled "DefaultValueAttribute" constants to a record.
+ // Note: evaluation of attribute-based value expressions (DefaultValueExpressionAttribute /
+ // UpdateValueExpressionAttribute) is intentionally not handled here -- that behavior is opt-in via
+ // the derived "ExpressionTableOperations" so the base type carries no type-registry dependency.
+ s_applyDefaultValues = BuildDefaultValueApplier();
// Generate a data table to be used for schema operations
s_tableSchema = new DataTable(s_tableName);
@@ -3232,6 +3273,46 @@ static TableOperations()
}
}
+ // Builds a compiled delegate that applies any modeled "DefaultValueAttribute" constant values to a record.
+ // This mirrors the constant-assignment semantics used for default values but, unlike the expression-based
+ // path, has no dependency on the Gemstone.Expressions type registry. Any error is deferred to invocation
+ // so an unrelated bad default value does not fail static initialization of the whole modeled table.
+ private static Action BuildDefaultValueApplier()
+ {
+ try
+ {
+ ParameterExpression record = LinqExpression.Parameter(typeof(T), "record");
+ List assignments = [];
+
+ foreach (PropertyInfo property in s_properties.Values)
+ {
+ if (!property.TryGetAttribute(out DefaultValueAttribute? defaultValueAttribute))
+ continue;
+
+ LinqExpression value = LinqExpression.Constant(defaultValueAttribute.Value, property.PropertyType);
+ assignments.Add(LinqExpression.Call(record, property.SetMethod!, value));
+ }
+
+ return assignments.Count == 0 ?
+ static _ => { } :
+ LinqExpression.Lambda>(LinqExpression.Block(assignments), record).Compile();
+ }
+ catch (Exception ex)
+ {
+ return _ => throw new ArgumentException($"Error evaluating \"{nameof(DefaultValueAttribute)}\" for a property of type \"{typeof(T).FullName}\": {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Gets the set of modeled record properties for type , i.e., the public
+ /// read/write properties that are not marked with .
+ ///
+ ///
+ /// Exposed for derived types, such as , that need to build
+ /// their own per-property behavior over the same property set used by the base table operations.
+ ///
+ protected static IEnumerable RecordProperties => s_properties.Values;
+
internal static bool IsSearchableField(string fieldName)
{
return s_searchableFields.Contains(fieldName);
@@ -3280,23 +3361,6 @@ private static (Regex, Func)[] ResolveExtensionAttributes
- /// Gets or sets instance used for evaluating encountered instances
- /// of the on modeled table properties.
- ///
- ///
- /// Accessing this property will create a unique type registry for the current type which
- /// will initially contain the values found in the
- /// and can be augmented with custom types. Set to null to restore use of the default type registry.
- ///
- public static TypeRegistry TypeRegistry
- {
- get => s_typeRegistry ??= ValueExpressionParser.DefaultTypeRegistry.Clone();
- set => s_typeRegistry = value;
- }
-
// Static Methods
///
@@ -3320,7 +3384,8 @@ public static TypeRegistry TypeRegistry
///
/// This method is useful to create a new type instance when no data connection
/// is available, applying any modeled default values as specified by a
- /// or on the model properties.
+ /// on the model properties. To also apply values, use
+ /// .
///
public static Func NewRecordFunction()
{
@@ -3335,7 +3400,8 @@ public static TypeRegistry TypeRegistry
///
/// This method is useful to apply defaults values to an existing type instance when no data
/// connection is available, applying any modeled default values as specified by a
- /// or on the model properties.
+ /// on the model properties. To also apply values, use
+ /// .
///
public static Action ApplyRecordDefaultsFunction()
{
@@ -3349,8 +3415,9 @@ public static Action ApplyRecordDefaultsFunction()
/// Delegate for the method.
///
/// This method is useful to apply update values to an existing type instance when no data
- /// connection is available, applying any modeled update values as specified by instances of the
- /// on the model properties.
+ /// connection is available. The base implementation performs no action; to apply
+ /// modeled update values as specified by instances of the on the
+ /// model properties, use .
///
public static Action ApplyRecordUpdatesFunction()
{
diff --git a/src/UnitTests/TableOperationsDefaultsTest.cs b/src/UnitTests/TableOperationsDefaultsTest.cs
new file mode 100644
index 000000000..f8ea05580
--- /dev/null
+++ b/src/UnitTests/TableOperationsDefaultsTest.cs
@@ -0,0 +1,113 @@
+//******************************************************************************************************
+// TableOperationsDefaultsTest.cs - Gbtc
+//
+// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
+//
+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
+// file except in compliance with the License. You may obtain a copy of the License at:
+//
+// http://opensource.org/licenses/MIT
+//
+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
+// License for the specific language governing permissions and limitations.
+//
+// Code Modification History:
+// ----------------------------------------------------------------------------------------------------
+// 06/25/2026 - J. Ritchie Carroll
+// Generated original version of source code.
+//
+//******************************************************************************************************
+
+using System;
+using System.ComponentModel;
+using System.Data;
+using System.Data.Common;
+using Gemstone.Data.Model;
+using Gemstone.Expressions.Model;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Gemstone.Data.UnitTests
+{
+ [TestClass]
+ public class TableOperationsDefaultsTest
+ {
+ // Sample model with a standard constant default value and a value-expression default value.
+ // The value expression uses only the always-registered "Guid" type, so it requires no external
+ // setup (e.g., Settings) -- this lets us verify the create path without a live database.
+ public class SampleModel
+ {
+ [PrimaryKey(true)]
+ public int ID { get; set; }
+
+ [DefaultValue(42)]
+ public int Code { get; set; }
+
+ [DefaultValueExpression("Guid.NewGuid()")]
+ public Guid Token { get; set; }
+ }
+
+ private static AdoDataConnection CreateConnection()
+ {
+ // Connect using a no-op connection type so no live database is required.
+ return new AdoDataConnection(null!, typeof(TestConnection));
+ }
+
+ [TestMethod]
+ public void BaseTableOperations_NewRecord_AppliesOnlyDefaultValueAttribute()
+ {
+ using AdoDataConnection connection = CreateConnection();
+
+ TableOperations table = new(connection);
+ SampleModel record = table.NewRecord();
+
+ Assert.IsNotNull(record);
+ Assert.AreEqual(42, record.Code); // DefaultValueAttribute applied
+ Assert.AreEqual(Guid.Empty, record.Token); // DefaultValueExpressionAttribute NOT applied
+ }
+
+ [TestMethod]
+ public void ExpressionTableOperations_NewRecord_AppliesValueExpressions()
+ {
+ using AdoDataConnection connection = CreateConnection();
+
+ ExpressionTableOperations table = new(connection);
+ SampleModel record = table.NewRecord();
+
+ Assert.IsNotNull(record);
+ Assert.AreEqual(42, record.Code); // DefaultValueAttribute applied
+ Assert.AreNotEqual(Guid.Empty, record.Token); // DefaultValueExpressionAttribute evaluated
+ }
+
+ [TestMethod]
+ public void BaseTableOperations_ApplyRecordDefaults_AppliesOnlyDefaultValueAttribute()
+ {
+ using AdoDataConnection connection = CreateConnection();
+
+ TableOperations table = new(connection);
+ SampleModel record = new();
+ table.ApplyRecordDefaults(record);
+
+ Assert.AreEqual(42, record.Code);
+ Assert.AreEqual(Guid.Empty, record.Token);
+ }
+
+ // No-op connection that allows constructing an AdoDataConnection without a live database.
+ public class TestConnection : DbConnection
+ {
+ public override string ConnectionString { get; set; } = string.Empty;
+ public override int ConnectionTimeout => 0;
+ public override string Database => string.Empty;
+ public override string DataSource => string.Empty;
+ public override string ServerVersion => string.Empty;
+ public override ConnectionState State => ConnectionState.Open;
+ public override void Open() { }
+ public override void Close() { }
+ protected override DbCommand CreateDbCommand() => null!;
+ protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => null!;
+ public override void ChangeDatabase(string databaseName) { }
+ }
+ }
+}