ND.FW.DataValidation
1.1.0
dotnet add package ND.FW.DataValidation --version 1.1.0
NuGet\Install-Package ND.FW.DataValidation -Version 1.1.0
<PackageReference Include="ND.FW.DataValidation" Version="1.1.0" />
<PackageVersion Include="ND.FW.DataValidation" Version="1.1.0" />
<PackageReference Include="ND.FW.DataValidation" />
paket add ND.FW.DataValidation --version 1.1.0
#r "nuget: ND.FW.DataValidation, 1.1.0"
#:package ND.FW.DataValidation@1.1.0
#addin nuget:?package=ND.FW.DataValidation&version=1.1.0
#tool nuget:?package=ND.FW.DataValidation&version=1.1.0
ND.FW.DataValidation
A configurable data-validation and metadata-derivation nuget, built on top of
ND.FW.RulesEngine. Everything
is driven by the same Rule JSON shape — no C# changes needed to add a validation
rule, switch rule providers, or add a metadata-derivation stage.
Install
dotnet add package ND.FW.DataValidation
The two halves
| Half | Entry point | Input | Output |
|---|---|---|---|
| Validation | IDataValidationService.ValidateAsync |
one record (IDictionary<string,object?>) |
pass/fail per rule |
| Metadata derivation | IMetadataDerivationService.DeriveAsync |
many AttributeCandidates (multi-page, multi-source) |
one canonical record + evidence + conflicts |
Both read rules through the same IRuleRepository and the same Rule JSON shape —
a rule's RuleExpressionType decides which half consumes it. Validation types
(Required/Mandatory, Regex, NumberRange, DateValidation, Conditional,
CrossField, Completeness, Table, Validation (duplicate-row), Document,
Lookup, Triangulation, FuzzyMatch, Enrichment, Standardization,
Normalization) map to an ND.FW.RulesEngine workflow exactly as before.
Metadata-derivation types (AttributeMapping, CandidateGrouping,
MultiPageConsolidation, ConfidenceAggregation, CanonicalSelection,
MultiValueConsolidation, AttributeDerivation, ConflictDetection,
CrossFieldValidation) drive the consolidation pipeline instead. You can mix both
kinds of rules in the same rule set returned by a provider — each service simply
filters for the types it understands.
Quick start
builder.Services.AddDataValidation(builder.Configuration, options =>
{
// register any product-specific operators alongside the built-in base set
options.CustomOperatorTypes.Add(typeof(MyProduct.Operators));
});
// appsettings.json
{
"DataValidation": {
"RuleRepository": {
"UseDatabaseProvider": true,
"UseLocalJsonProvider": true,
"Mode": "Additive"
},
"Database": {
"Schema": "dbo",
"RuleDetailsSpName": "sp_Get_Rule_Details",
"RuleJsonColumnName": "Rule_Definition_JSON",
"ParameterMap": {
"FileTaxonomyId": "@p_File_Taxonomy_Id",
"DocTaxonomyId": "@p_Doc_Taxonomy_Id",
"FileDocTaxonomyId": "@p_File_Doc_Taxonomy_Id",
"FieldId": "@p_File_Taxonomy_X_Doc_Taxonomy_X_Field_Id",
"RuleEventStage": "@p_Rule_Event_Stage"
}
},
"LocalJson": {
"RootDirectory": "RuleSets",
"FileNameTemplate": "{DocTaxonomyId}.json"
}
}
}
Validation
var criteria = RuleQueryCriteria.FromKeys(
("FileTaxonomyId", 12),
("DocTaxonomyId", 4),
("FieldId", 101));
var input = new Dictionary<string, object?> { ["phone"] = "+919876543210", ["pan"] = "ABCDE1234F" };
var result = await dataValidationService.ValidateAsync(criteria, input);
foreach (var r in result.RuleResults)
Console.WriteLine($"{r.RuleName}: {(r.IsSuccess ? "PASS" : $"FAIL - {r.ErrorMessage}")}");
Metadata derivation
var candidates = new List<AttributeCandidate>
{
new() { AttributeName = "PT_NAME_1", Value = "John A Smith", Page = 1, Position = 0, ValueStrength = 0.81, Source = "ocr" },
new() { AttributeName = "patient full name", Value = "John Smith", Page = 3, Position = 2, ValueStrength = 0.93, Source = "model-x" },
new() { AttributeName = "PhoneNumber", Value = "555-0100", Page = 1, ValueStrength = 0.7 },
new() { AttributeName = "PhoneNumber", Value = "555-0199", Page = 2, ValueStrength = 0.6 },
};
var result = await metadataDerivationService.DeriveAsync(criteria, candidates);
var patientName = result.Attributes["PatientName"]; // canonical value + evidence + confidence
foreach (var conflict in result.Conflicts)
Console.WriteLine($"Conflict on {conflict.AttributeName}: {conflict.ConflictingValues.Count} candidate values");
Rule providers — multiple, at the same time
IRuleRepository has two built-in implementations:
DatabaseRuleRepository— calls a stored procedure. SP name, schema, and the mapping fromRuleQueryCriteria.Keysto SP parameter names are all configuration (DatabaseRuleProviderOptions), not hardcoded — so this provider works for any product's own key shape, not just taxonomy IDs.LocalJsonRuleRepository— reads rule JSON from local files (or inline configuration). Accepts a single Rule object, a Rule array, or a{ "Rules": [...] }wrapper per file.
When both are enabled, CompositeRuleRepository merges their results:
Additive(default) — rules from every enabled provider are combined, de-duplicated byRuleName(earliest-registered provider wins on a collision).Fallback— providers are tried in order; the first one that returns any rules wins and the rest are skipped.
Add a third provider (e.g. a remote config service) by implementing
IRuleRepository and registering it — CompositeRuleRepository accepts any
IEnumerable<IRuleRepository>.
Metadata-derivation pipeline
Stages run in a fixed logical order, but only the stages whose
RuleExpressionType is present in the resolved rule set actually run — a rule
set with just CandidateGrouping + CanonicalSelection rules skips multi-page
consolidation and cross-field validation entirely.
AttributeMapping → CandidateGrouping → MultiPageConsolidation →
ConfidenceAggregation → CanonicalSelection → MultiValueConsolidation →
AttributeDerivation → ConflictDetection → CrossFieldValidation
New in 1.1.0 — additional validation RuleExpressionTypes
Added to support rule sheets authored with Mandatory/CrossField/Completeness/
Table/Validation/Document types and Lookup rules using
LookupCode/MatchType/MinimumConfidence/ReturnTop/SuggestionOnly naming.
All additions are purely additive on RuleExpression — no existing field was
renamed, retyped, or removed, so previously authored rule rows/JSON keep
deserializing unchanged.
| RuleExpressionType | Expression shape | Notes |
|---|---|---|
Mandatory |
same as Required |
alias; identical behavior to Required |
CrossField |
InputFields (group, e.g. exactly-one-selected) or Fields (pair) + Operator |
pair operators (SAME_MASTER_RECORD, DESCRIPTION_MATCH) currently only check presence-consistency, not true master-record/description agreement — see caveat below |
Completeness |
Inputs (or InputFields) |
passes if at least one is non-empty |
Table |
RequiredColumns, When: "ROW_PRESENT" |
requires the caller to pass one row's column values per execution — this nuget validates a single row per call, it does not iterate a table itself |
Validation (duplicate-row) |
Table, UniqueBy |
requires the caller's context to expose a pre-built "<Table>_UniqueKeys" collection — composite-key construction from UniqueBy columns across rows is the caller's responsibility |
Document |
Package, RequiredDocuments, ConditionalDocuments |
requires the caller's context to expose "<DocCode>_Present" per required document; ConditionalDocuments is carried through for traceability only and is not itself enforced |
Lookup (extended) |
LookupCode, MatchType, MinimumConfidence, ReturnTop, SuggestionOnly |
aliases of ReferenceDataset, (new) match-mode, MinimumScore, (new) result cap, (new) suggestion-only flag; the original field names still work and take precedence when both are present |
Standardization (executed) |
Operations: [...] |
when present, attaches a StandardizationPlugin reference that is expected to write the transformed value to "<InputField>_Standardized" (or OutputField, if set) and always passes; a Standardization rule with no Operations keeps the old always-pass, no-op behavior for backward compatibility |
Important — plugin implementations are not included. Exactly like the
existing Lookup/Triangulation/FuzzyMatch/Enrichment types, which only
attach a "ReferenceLookupPlugin" reference and rely on something else (the
consuming service or NdRulesEngine itself) to actually implement that plugin,
the new Standardization-with-Operations path attaches a
"StandardizationPlugin" reference by name — this nuget does not ship a
StandardizationPlugin implementation. Operators.ApplyStandardization(...)
is provided as the transform logic you'd call from inside that plugin, but
someone still has to register a plugin named StandardizationPlugin with
NdRulesEngine (the same way ReferenceLookupPlugin is registered today)
before a Standardization rule with Operations will do anything beyond
attach the reference.
Also note:
CrossField's pair-comparison operators (SAME_MASTER_RECORD,DESCRIPTION_MATCH) reduce to a presence-consistency check (Operators.CrossFieldPresenceConsistent) — both fields present or both blank. Verifying that two codes truly resolve to the same master record or matching description requires reference-data access, which belongs in a Lookup-style plugin, not this stateless expression operator. Treat this as a first-pass presence check, not full semantic equivalence, until/unless a dedicated plugin is wired in.TableandValidation(duplicate-row) rules validate one row's data at a time (or one pre-built key list, for duplicates) perValidateAsynccall — this nuget has no concept of iterating a repeating table's rows on its own. The calling service is responsible for looping over rows and callingValidateAsyncper row (same patternDataPostProcessingServicealready uses to loop overreviewItem.values).
| Stage | RuleExpressionType | What it does |
|---|---|---|
| Attribute name mapping | AttributeMapping |
Renames raw extracted names onto one canonical name, via AttributeNameMap. |
| Candidate grouping | CandidateGrouping |
Groups candidates by (mapped) attribute name, optionally narrowed by GroupByKeys. |
| Multi-page consolidation | MultiPageConsolidation |
Orders each group by page/position per PageOrderStrategy. |
| Confidence aggregation | ConfidenceAggregation |
Scores each candidate from key/position/value-strength via ConfidenceWeights. |
| Canonical value selection | CanonicalSelection |
Picks the winning value per attribute (or ranks candidates for multi-value attributes), via ConflictResolutionStrategy. |
| Multi-value consolidation | MultiValueConsolidation |
For attributes with IsMultiValue: true, keeps a de-duplicated, ranked list capped at MaxValues. |
| Attribute derivation | AttributeDerivation |
Computes a new canonical attribute from one or more different, already-canonical attributes, via DerivationOperator + DerivationArgs. |
| Conflict detection | ConflictDetection |
Flags attributes whose top candidates differ by less than ConflictThreshold. |
| Cross-field validation | CrossFieldValidation |
Runs an ND.FW.RulesEngine expression (Condition) against the canonical record — reuses the same Operators.* as ordinary validation. |
Deriving a new attribute from other attributes
AttributeDerivation is the piece that answers "from one attribute, derive
another" — as opposed to CanonicalSelection, which reconciles multiple
observations of the same attribute. It reads whichever canonical attributes
have already been produced (by InputFields, in order) and writes a new one
(OutputField), using a built-in DerivationOperator:
| Operator | DerivationArgs | Example |
|---|---|---|
Concat |
Separator, SkipBlanks |
FullName from FirstName + LastName |
Format |
uses OutputFormat (composite format string) |
"{0}, {1}" from City + State |
Arithmetic |
Expression (e.g. "{0} * {1} / 100") |
TaxAmount from Price + TaxPercent |
DateDiff |
Unit (Years|Months|Days) |
Age from DateOfBirth (+ optional second InputFields entry as the anchor date; defaults to today) |
Substring |
Start, Length |
Extract a fixed-position code from a longer field |
Coalesce |
— | First non-blank value across several candidate fields |
MapLookup |
Map (object), Default |
GenderLabel from a Gender code ("M" → "Male") |
Case |
Mode (Upper|Lower|Title) |
Normalize casing |
Trim |
— | Strip whitespace |
RegexExtract |
uses Pattern, plus Group |
Pull a substring matching a pattern |
{
"RuleName": "DerivePatientFullName",
"ErrorMessage": "",
"RuleExpressionType": "AttributeDerivation",
"Expression": {
"InputFields": ["FirstName", "LastName"],
"OutputField": "PatientFullName",
"DerivationOperator": "Concat",
"DerivationArgs": { "Separator": " ", "SkipBlanks": true }
}
}
{
"RuleName": "DerivePatientAge",
"ErrorMessage": "",
"RuleExpressionType": "AttributeDerivation",
"Expression": {
"InputFields": ["DateOfBirth"],
"OutputField": "PatientAge",
"DerivationOperator": "DateDiff",
"DerivationArgs": { "Unit": "Years" }
}
}
The derived attribute lands in MetadataDerivationResult.Attributes["PatientFullName"]
just like any other canonical attribute — same Evidence/AggregatedConfidence
shape (evidence here points back at the source attributes it was computed from,
Source: "derived-from:FirstName" etc., confidence fixed at 1.0 since it's a
deterministic computation, not a scoring decision) — and flows into
ToCanonicalDictionary() for CrossFieldValidation or downstream consumers the
same way. Derivation rules can chain within one rule set (a later
AttributeDerivation rule can reference an earlier rule's OutputField), since
rules of the same stage run in the order they appear.
Because InputFields are read from context.Result.Attributes — which is filled
in by CanonicalSelection/MultiValueConsolidation, which run before
AttributeDerivation in the fixed stage order — a derivation rule can only see
attributes those stages (or an earlier AttributeDerivation rule) have already
resolved. SourcePriorityCanonicalSelection currently runs after
CrossFieldValidation (i.e. after AttributeDerivation too), so an InputFields
entry that's only ever produced by that stage won't be visible yet; keep that in
mind when combining rule types.
If a built-in operator doesn't cover what you need (e.g. a currency conversion
against a live rate table, or fuzzy name matching), the fallback is the same one
the nuget already documents for any new stage type — implement
IMetadataDerivationStage under a new RuleExpressionType and register it via
options.MetadataDerivationStageTypes.Add(typeof(YourStage)). No existing stage
needs to change.
Note:
ToCanonicalDictionary()exposes multi-value attributes (IsMultiValue: true) as aList<object?>, not a scalar. ACrossFieldValidationexpression referencing such an attribute must treat it as a list (e.g.((List<object?>)input1["PhoneNumber"]).Count > 0) rather than comparing it directly —ND.FW.RulesEnginepasses the value through unchanged and does not coerce it to a string or number.
Every canonical attribute retains full evidence (EvidenceRef: value, page,
position, source location, source, and computed weight) for every candidate that
contributed to it, whether it won or lost — satisfying audit/traceability needs
without any extra configuration.
Extending with a custom stage
Metadata-derivation stages are just another named plugin point, exactly like
ND.FW.RulesEngine's INdContextPlugin:
public sealed class MyCustomStage : IMetadataDerivationStage
{
public string RuleExpressionType => "MyCustomStageType";
public Task ExecuteAsync(Rule rule, MetadataDerivationContext context, CancellationToken ct) { ... }
}
services.AddDataValidation(configuration, options =>
{
options.MetadataDerivationStageTypes.Add(typeof(MyCustomStage));
});
Rule JSON compatibility
Existing rule rows/files are not restructured. Every new capability is added by:
- New
RuleExpressionTypestring values (data, not schema). - A small number of new, optional fields on
RuleExpression—AttributeNameMap,GroupByKeys,ConfidenceWeights,ConflictResolutionStrategy,ConflictThreshold,IsMultiValue,MaxValues,PageOrderStrategy,CanonicalFields,DerivationOperator,DerivationArgs. All nullable; absent in existing rows and simply unused by validation rule types. - Reuse of one existing field,
Condition, as the raw cross-field expression string forCrossFieldValidationrules (this field already existed onRuleExpressionand was previously unused by any mapped type).
No existing field was renamed, retyped, or removed.
Packaging note
ND.FW.DataValidation references ND.FW.RDBM / Microsoft.Data.SqlClient directly
so DatabaseRuleRepository is available out of the box, matching how DVS already
depends on ND.FW.RDBM. If a consumer only ever wants the local-JSON provider (no
database dependency at all — e.g. an edge/offline scenario), the cleaner long-term
shape is to split this into ND.FW.DataValidation.Core (abstractions, local JSON,
metadata derivation) + ND.FW.DataValidation.SqlServer (the database provider) so
the SQL dependency is opt-in per package reference rather than always-transitive.
That split is a bigger packaging change than this version takes on; call it out if
a consumer without SQL Server ever needs this nuget.
License
Internal use.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
- ND.FW.RDBM (>= 0.0.25)
- ND.FW.RulesEngine (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.