The manager maintains a thread-safe in-memory profile registry keyed by "datasource::entity". When Apply() is called, every FieldDefaultRule in the matching EntityDefaultsProfile is evaluated: expressions (starting with ":") are dispatched to the resolver pipeline; literals are used as-is.
// 1. Initialize (idempotent — safe to call multiple times)
DefaultsManager.Initialize(dmeEditor);
// 2. Build a profile for the "Users" entity
var profile = EntityDefaultsProfile.For("Users")
.Set("Id", ":NEWGUID") // expression — new Guid each record
.Set("CreatedAt", ":NOW") // expression — current DateTime
.Set("CreatedBy", ":USERNAME") // expression — current OS user
.Set("Status", "Active"); // literal — used as-is
// 3. Register it (thread-safe, replaces any existing profile for "mydb::Users")
DefaultsManager.RegisterProfile("mydb", "Users", profile);
// 4a. Apply to a Dictionary record
var record = new Dictionary<string, object> { ["Name"] = "Alice" };
DefaultsManager.Apply(dmeEditor, "mydb", "Users", record);
// record["Id"] = "3f2504e0-..."
// record["CreatedAt"] = 2026-04-03 12:00:00
// record["Status"] = "Active"
// 4b. Apply to a POCO (reflection-based)
var user = new User { Name = "Alice" };
DefaultsManager.Apply(dmeEditor, "mydb", "Users", user);
// 4c. Apply to a DataRow
DataRow row = myTable.NewRow();
DefaultsManager.Apply(dmeEditor, "mydb", "Users", row);
Rule String Convention
The rule string passed to FieldDefaultRule (and to Resolve()) follows a simple prefix convention:
Prefix
Meaning
Example
:
Expression — parsed and dispatched to the resolver pipeline at runtime
":NOW", ":USERNAME", ":NEWGUID"
(none)
Literal — used as-is without any resolution
"Active", "1", "pending"
Inside the expression body (after the leading ":"), tokens can use two syntaxes:
Syntax
Example
Notes
Function style (v2 canonical)
ADDDAYS(TODAY, 7)
Recommended for new code
Dot-style DSL (v1 legacy)
ADDDAYS.TODAY.7
Automatically normalized by RuleNormalizer
Note on ApplyOnlyIfNull — FieldDefaultRule has a third constructor parameter (default true). When true the rule is skipped if the field already has a non-null, non-empty value. Pass false to always overwrite.
EntityDefaultsProfile — Fluent Builder
Building a profile
// Factory entry point
var profile = EntityDefaultsProfile.For("Orders")
.Set("OrderId", ":NEWGUID", applyOnlyIfNull: true) // expression, skip if set
.Set("OrderDate", ":NOW") // expression, skip if set
.Set("Status", "Pending") // literal
.Set("CreatedBy", ":USERNAME") // expression
.Set("Version", "1", applyOnlyIfNull: false); // literal, always overwrite
DefaultsManager.RegisterProfile("OrdersDB", "Orders", profile);
FieldDefaultRule properties
Property
Type
Description
FieldName
string
Target field name; matched case-insensitively
RuleString
string
Rule text including the : prefix for expressions
ApplyOnlyIfNull
bool
When true (default), skip if field already has a value
IsExpression
bool
Computed: true when RuleString starts with ":"
ExpressionBody
string
Rule text stripped of the leading ":", passed to the resolver pipeline
EntityDefaultsProfile properties
Property
Type
Description
EntityName
string
Entity/table name this profile applies to
Rules
IReadOnlyList<FieldDefaultRule>
Ordered list of field rules; evaluated in declaration order
Fluent Set method
// Replaces any existing rule for the same fieldName (case-insensitive)
EntityDefaultsProfile Set(string fieldName, string ruleString, bool applyOnlyIfNull = true)
Apply API
All three overloads look up the profile registered under datasource::entity, iterate its rules, and write resolved values to the target.
Only columns that exist in row.Table.Columns are processed. Missing columns in the schema are silently skipped.
Return value
All overloads return IErrorsInfo:
Flag
Meaning
Errors.Ok
All rules applied successfully (or no profile registered)
Errors.Warning
One or more fields threw during resolution — partial apply
Resolve & Validate API
Resolve a single rule string
// Call-site resolution without a profile
object value = DefaultsManager.Resolve(editor, ":NOW");
object guid = DefaultsManager.Resolve(editor, ":NEWGUID");
object lit = DefaultsManager.Resolve(editor, "Active"); // literal — returned unchanged
TestRule — resolve + error info
// Useful for validation UIs and preview tooling
var (info, value) = DefaultsManager.TestRule(editor, ":ADDDAYS(TODAY, 30)");
if (info.Flag == Errors.Ok)
Console.WriteLine($"Resolved: {value}");
else
Console.WriteLine($"Error: {info.Message}");
ValidateRule — syntax check only
IErrorsInfo result = DefaultsManager.ValidateRule(editor, ":ADDDAYS(TODAY, 30)");
// Checks syntax without executing the resolver
GetAvailableResolvers
// Returns map of resolverName → example rule tokens
var resolvers = DefaultsManager.GetAvailableResolvers(editor);
foreach (var (name, examples) in resolvers)
Console.WriteLine($"{name}: {string.Join(", ", examples)}");
Profile Registry
The profile registry is a static Dictionary<string, EntityDefaultsProfile> guarded by a lock object. Keys are "datasource::entity" (case-insensitive).
// Register (or replace) a profile
DefaultsManager.RegisterProfile("mydb", "Users", profile);
// Retrieve an existing profile (returns null if not found)
EntityDefaultsProfile p = DefaultsManager.GetProfile("mydb", "Users");
// Remove a profile (no-op if missing)
DefaultsManager.RemoveProfile("mydb", "Users");
Thread safety — RegisterProfile, GetProfile, and RemoveProfile all acquire a shared lock. Profiles themselves (EntityDefaultsProfile) are immutable once built — the fluent builder returns a new instance via For() and each Set() mutates and returns the same builder.
Built-in Resolvers
All resolvers extend BaseDefaultValueResolver and are auto-registered into DefaultValueResolverManager on initialization.
DateTime resolver
Token / Expression
Returns
:NOW / :CURRENTDATETIME
DateTime.Now
:TODAY / :CURRENTDATE
DateTime.Today
:YESTERDAY / :TOMORROW
±1 day from today
:CURRENTTIME
DateTime.Now.TimeOfDay
:STARTOFMONTH / :ENDOFMONTH
First/last day of current month
:STARTOFYEAR / :ENDOFYEAR
Jan 1 / Dec 31 of current year
:STARTOFWEEK / :ENDOFWEEK
Monday / Sunday of current week
:ADDDAYS(TODAY, 7)
Seven days from today
:ADDHOURS(NOW, 2)
Two hours from now
:ADDMONTHS(TODAY, 1)
One month from today
:ADDYEARS(TODAY, -1)
One year ago
:FORMAT(NOW, 'yyyy-MM-dd')
Formatted date string
Guid resolver
Token / Expression
Returns
:NEWGUID / :GUID / :UUID / :GENERATEUNIQUEID
New GUID with hyphens (default D format)
:GUID(N)
GUID without hyphens: 3f2504e04f8941d39a0c0305e82c3301
:GUID(D)
Default: 3f2504e0-4f89-41d3-9a0c-0305e82c3301
:GUID(B)
Braces: {3f2504e0-...}
:GUID(P)
Parentheses: (3f2504e0-...)
UserContext resolver
Token
Returns
:USERNAME / :CURRENTUSER / :USERLOGIN
Environment.UserName
:USERID
Current user SID / identifier
:USEREMAIL
User email (if available)
:USERDOMAIN
Environment.UserDomainName
:USERPROFILE
User profile directory path
:USERGROUP
Primary user group
:USERROLE
User role (from context parameters if provided)
:USERPRINCIPAL
Windows principal name
SystemInfo resolver
Token
Returns
Cacheable
:MACHINENAME / :HOSTNAME
Environment.MachineName
Yes
:VERSION
.NET version string
Yes
:APPVERSION
Entry assembly version
Yes
:OSVERSION
OS version string
Yes
:PLATFORM
OS platform string
Yes
:PROCESSORCOUNT
Number of processors
Yes
:WORKINGSET
Process working set bytes
No
:TIMESTAMP
Unix epoch (seconds)
No
:TICKS
DateTime.Now.Ticks
No
Environment resolver
Token / Expression
Returns
:TEMP / :TEMPPATH
%TEMP% path
:SYSTEMPATH
System PATH variable
:USERPATH
User-scope PATH
:ENV(VARNAME) / :ENVIRONMENTVARIABLE(VARNAME)
Named environment variable value
:ENV:VARNAME
Key-value shorthand
Formula resolver
Token / Expression
Returns
:SEQUENCE(1000)
Sequence starting at 1000
:INCREMENT(FieldName)
Field value incremented by 1
:RANDOM(1, 100)
Random integer in [1, 100]
:ADD(a, b)
a + b
:SUBTRACT(a, b) / :SUB(a, b)
a - b
:MULTIPLY(a, b) / :MUL(a, b)
a × b
:DIVIDE(a, b) / :DIV(a, b)
a ÷ b (0 on divide-by-zero)
:ROUND(val, digits)
Rounded value
:CALCULATE(expr) / :COMPUTE(expr)
General arithmetic expression
Expression resolver (conditionals)
Token / Expression
Returns
:IF(condition, thenVal, elseVal)
Ternary branch
:ISNULL(value, fallback)
value if not null, else fallback
:COALESCE(a, b, c)
First non-null argument
:CASE(value, when1, then1, when2, then2, default)
Multi-branch switch
:EQ(a, b) / :NE(a,b)
Equality / inequality (bool)
:GT(a, b) / :LT(a, b)
Numeric comparison (bool)
:AND(a, b) / :OR(a, b) / :NOT(a)
Logical operators
Configuration resolver
Token / Expression
Returns
:CONFIG(KeyName) / :CONFIGURATIONVALUE(KeyName)
App config value by key
:APPSETTING(KeyName) / :SETTING(KeyName)
AppSettings value
:CONNECTIONSTRING(Name)
Named connection string
:CONFIG:KeyName
Key-value shorthand
DataSource resolver
Queries any registered IDataSource via GetEntity + AppFilter. Aggregates are computed in-memory via LINQ.
Filter values prefixed with @ are bound from the runtime IPassedArgs context parameters.
ObjectProperty resolver
Extracts values from runtime objects passed in IPassedArgs via reflection.
Token / Expression
Returns
:PROPERTY(propName) / :GETPROPERTY(propName)
Named public property from context object
:FIELD(fieldName)
Public field from context object
:NESTED(obj.sub.prop)
Dot-path traversal
:ARRAYITEM(propName, index)
Array element by index
:DICTVALUE(propName, key)
Dictionary entry by key
Resolver capabilities
All resolvers implement IResolverCapabilities:
Property
Default
Description
Priority
100
Lower values are tried first in the pipeline
IsDeterministic
false
If true, the resolver output may be cached
SupportsCaching
= IsDeterministic
Whether the manager may cache this resolver's output
SupportsAsync
false
Reserved for future async resolver support
Dot-Style DSL (v1)
The DotStyleRuleParser understands a legacy dot-segment format where the first segment is the operator and subsequent segments are arguments. RuleNormalizer converts this to function style before handing off to the resolver pipeline.
A rule is detected as dot-style when its first segment (before the first dot) matches one of the known operators. If a parenthesis appears before any dot, the rule is treated as function style.
Known operator set
DateTime, Formula/math, Expression/conditionals, Query/datasource, User/system, Object property operators — see DotStyleRuleParser._knownOperators for the full list.
Creating a Custom Resolver
Extend BaseDefaultValueResolver and register it via RegisterCustomResolver.
public class BusinessResolver : BaseDefaultValueResolver
{
public BusinessResolver(IDMEEditor editor) : base(editor) { }
public override string ResolverName => "Business";
public override int Priority => 50; // run before built-ins (100)
public override bool IsDeterministic => false; // values vary per call
public override IEnumerable<string> SupportedRuleTypes => new[]
{
"NEXTORDERID", "CUSTOMERCODE", "SALESREP"
};
public override bool CanHandle(string rule)
{
var upper = rule?.ToUpperInvariant().Trim() ?? "";
return upper.StartsWith("NEXTORDERID") ||
upper.StartsWith("CUSTOMERCODE") ||
upper.StartsWith("SALESREP");
}
public override object ResolveValue(string rule, IPassedArgs parameters)
{
return rule.ToUpperInvariant().Trim() switch
{
"NEXTORDERID" => GetNextOrderId(),
"CUSTOMERCODE" => GenerateCustomerCode(),
var r when r.StartsWith("SALESREP(") => GetSalesRep(r),
_ => null
};
}
public override IEnumerable<string> GetExamples() => new[]
{
"NEXTORDERID - next sequential order ID",
"CUSTOMERCODE - generated customer code",
"SALESREP(region) - default sales rep for a region"
};
private string GetNextOrderId() { /* ... */ return "ORD-001"; }
private string GenerateCustomerCode() { /* ... */ return "CUST-0042"; }
private string GetSalesRep(string rule) { /* ... */ return "John Smith"; }
}
// Registration:
DefaultsManager.RegisterCustomResolver(editor, new BusinessResolver(editor));
Legacy / Persistence API
These methods delegate to DefaultValueHelper and operate on DefaultValue entries stored in ConnectionProperties.DatasourceDefaults. They provide backward compatibility with code written against the original DataSyncManager/configuration-based defaults system.
// Get all persisted defaults for a data source
List<DefaultValue> defaults = DefaultsManager.GetDefaults(editor, "MyDatabase");
// Save a list of defaults back to config
DefaultsManager.SaveDefaults(editor, defaults, "MyDatabase");
// Set a single column default (isRule=false → static literal; isRule=true → rule string)
DefaultsManager.SetColumnDefault(editor, "MyDatabase", "Users", "Status", "Active", isRule: false);
DefaultsManager.SetColumnDefault(editor, "MyDatabase", "Users", "CreatedAt", "NOW", isRule: true);
DefaultsManager.SetColumnDefault(editor, "MyDatabase", "Users", "Id", "NEWGUID", isRule: true);
// Retrieve a persisted default
DefaultValue def = DefaultsManager.GetColumnDefault(editor, "MyDatabase", "Users", "Status");
// Apply persisted defaults to a record
DefaultsManager.ApplyDefaults(editor, "MyDatabase", "Users", record);
Recommendation — for new code prefer the EntityDefaultsProfile + RegisterProfile pattern, which is type-safe, fluent, and does not require persistence. Use the legacy API when you need defaults to survive application restarts via the config file.
Dependency Injection Interface
IDefaultsManager mirrors the static DefaultsManager surface for DI-friendly scenarios.
// Registration (example with Microsoft.Extensions.DependencyInjection)
services.AddSingleton<IDefaultsManager>(sp =>
{
var editor = sp.GetRequiredService<IDMEEditor>();
var mgr = new DefaultsManager();
mgr.Initialize(editor);
return mgr;
});
// Usage in a service
public class OrderService
{
private readonly IDefaultsManager _defaults;
private readonly IDMEEditor _editor;
public OrderService(IDefaultsManager defaults, IDMEEditor editor)
{
_defaults = defaults;
_editor = editor;
}
public void CreateOrder(Order order)
{
_defaults.Apply(_editor, "OrdersDB", "Orders", order);
// order.Id, CreatedAt, CreatedBy, Status are now populated
}
}
IDefaultsManager method summary
Method
Description
Initialize(editor)
Init or re-init the manager
RegisterProfile(ds, entity, profile)
Register an EntityDefaultsProfile
GetProfile(ds, entity)
Retrieve a registered profile
RemoveProfile(ds, entity)
Remove a profile
Apply(editor, ds, entity, dict)
Apply to Dictionary<string,object>
Apply<T>(editor, ds, entity, poco)
Apply to POCO
Apply(editor, ds, entity, row)
Apply to DataRow
Resolve(editor, rule)
Resolve a single rule string
TestRule(editor, rule)
Resolve + return error info
ValidateRule(editor, rule)
Syntax check without execution
RegisterCustomResolver(editor, resolver)
Add a custom resolver to the pipeline
GetAvailableResolvers(editor)
List all resolvers with examples
GetDefaults / SaveDefaults
Legacy persistence API
SetColumnDefault / GetColumnDefault
Legacy per-column helpers
Examples
Audit fields pattern
// Common audit pattern for any entity
var auditProfile = EntityDefaultsProfile.For("Orders")
.Set("Id", ":NEWGUID")
.Set("CreatedAt", ":NOW")
.Set("CreatedBy", ":USERNAME")
.Set("UpdatedAt", ":NOW", applyOnlyIfNull: false) // always stamp
.Set("UpdatedBy", ":USERNAME", applyOnlyIfNull: false) // always stamp
.Set("Status", "Draft");
DefaultsManager.RegisterProfile("OrdersDB", "Orders", auditProfile);
var order = new Order { CustomerId = "C001", Total = 99.99m };
DefaultsManager.Apply(dmeEditor, "OrdersDB", "Orders", order);
DataSource lookup default
// Default the SalesRepId field by looking up the active rep for a region
// Filter value @Region is bound from IPassedArgs.Parameters at runtime
var profile = EntityDefaultsProfile.For("Invoices")
.Set("SalesRepId", ":LOOKUP(CrmDB, SalesReps, Id, Region, @Region)")
.Set("InvoiceDate", ":TODAY")
.Set("DueDate", ":ADDDAYS(TODAY, 30)");
DefaultsManager.RegisterProfile("InvoicesDB", "Invoices", profile);
// At call-site, pass the region via IPassedArgs:
var ctx = new PassedArgs();
ctx.ParameterString1 = "EU"; // e.g. bound to @Region by resolver
DefaultsManager.Apply(dmeEditor, "InvoicesDB", "Invoices", invoiceRecord, ctx);
Custom resolver + profile composition
// Register custom resolver first
DefaultsManager.RegisterCustomResolver(dmeEditor, new BusinessResolver(dmeEditor));
// Then build a profile that uses it
var profile = EntityDefaultsProfile.For("Tickets")
.Set("TicketId", ":NEXTORDERID") // handled by BusinessResolver
.Set("AssignedTo", ":SALESREP(EU)") // handled by BusinessResolver
.Set("CreatedAt", ":NOW")
.Set("Priority", "Normal");
DefaultsManager.RegisterProfile("HelpDesk", "Tickets", profile);
Validate and test rules before registration
string[] rulesToCheck = { ":ADDDAYS(TODAY, 7)", ":NEWGUID", ":ENV(APPDATA)" };
foreach (var rule in rulesToCheck)
{
var syntax = DefaultsManager.ValidateRule(dmeEditor, rule);
if (syntax.Flag != Errors.Ok)
{
Console.WriteLine($"Syntax error in '{rule}': {syntax.Message}");
continue;
}
var (info, value) = DefaultsManager.TestRule(dmeEditor, rule);
Console.WriteLine($"{rule} → {value} [{info.Flag}]");
}
List all resolvers and their tokens
var resolvers = DefaultsManager.GetAvailableResolvers(dmeEditor);
foreach (var (name, examples) in resolvers.OrderBy(x => x.Key))
{
Console.WriteLine($"\n{name}:");
foreach (var ex in examples)
Console.WriteLine($" {ex}");
}
Resolver names → example strings from all registered resolvers.
ValidateResolvers()
Validates each resolver for non-null name/types/examples.
ResolverExecutionResult (Phase 5 telemetry)
public sealed class ResolverExecutionResult
{
public string ResolverName { get; init; } // Winning resolver or "none"
public string OriginalRule { get; init; } // Verbatim rule string
public string NormalizedRule { get; init; } // After dot-style normalization
public object ResolvedValue { get; init; } // Value or null on failure
public bool Succeeded { get; init; } // True when resolver produced value
public bool FallbackUsed { get; init; } // Primary failed, fallback value used
public TimeSpan Duration { get; init; } // Wall-clock time spent resolving
public string ErrorMessage { get; init; } // Exception message when Succeeded=false
public string RuleFingerprint { get; init; } // 8-char FNV-1a hex hash of NormalizedRule
}
ResolverContext
public class ResolverContext
{
public IPassedArgs PassedArgs { get; }
public Dictionary<string, object> Extras { get; }
public ResolverContext(IPassedArgs passedArgs);
public ResolverContext(IPassedArgs passedArgs, Dictionary<string, object> extras);
public T GetExtra<T>(string key, T defaultValue = default);
// Computed shortcuts:
public string EntityName => PassedArgs?.CurrentEntity;
public string FieldName => /* Extras["FieldName"] ?? PassedArgs.ParameterString1 */;
}
public class DefaultResolverRegistry
{
public DefaultResolverRegistry(IDMEEditor editor);
// Scans assemblies for [DefaultResolver] classes, instantiates, registers via DefaultsManager
public void Discover();
public IReadOnlyDictionary<string, DefaultResolverDescriptor> GetDescriptors();
public bool TryGet(string resolverName, out DefaultResolverDescriptor descriptor);
public IDefaultValueResolver Create(string resolverName); // Creates new instance
}
public sealed class DefaultResolverDescriptor
{
public DefaultResolverAttribute Attribute { get; }
public Type ImplementationType { get; }
}