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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ND.FW.DataValidation" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ND.FW.DataValidation" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="ND.FW.DataValidation" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ND.FW.DataValidation --version 1.1.0
                    
#r "nuget: ND.FW.DataValidation, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ND.FW.DataValidation@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ND.FW.DataValidation&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=ND.FW.DataValidation&version=1.1.0
                    
Install as a Cake Tool

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 from RuleQueryCriteria.Keys to 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 by RuleName (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.
  • Table and Validation (duplicate-row) rules validate one row's data at a time (or one pre-built key list, for duplicates) per ValidateAsync call — 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 calling ValidateAsync per row (same pattern DataPostProcessingService already uses to loop over reviewItem.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 a List<object?>, not a scalar. A CrossFieldValidation expression 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.RulesEngine passes 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:

  1. New RuleExpressionType string values (data, not schema).
  2. A small number of new, optional fields on RuleExpressionAttributeNameMap, GroupByKeys, ConfidenceWeights, ConflictResolutionStrategy, ConflictThreshold, IsMultiValue, MaxValues, PageOrderStrategy, CanonicalFields, DerivationOperator, DerivationArgs. All nullable; absent in existing rows and simply unused by validation rule types.
  3. Reuse of one existing field, Condition, as the raw cross-field expression string for CrossFieldValidation rules (this field already existed on RuleExpression and 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 34 8/24/2026
1.0.2 107 8/17/2026
1.0.1 93 8/12/2026
1.0.0 90 8/12/2026