Modern business apps often need to store dynamic attributes per entity: customer custom fields, product specifications, survey responses, feature flags, metadata, tags, etc. Hard-coding columns for every new attribute leads to constant schema changes and maintenance pain. An Attribute-Value-Store (AVS) (also called EAV — Entity-Attribute-Value) lets you store dynamic fields in a structured, queryable way inside SQL while preserving performance, integrity, and reporting capability.
This article gives a complete, production-grade blueprint for designing an AVS engine built on top of a relational database (SQL Server style), integrated with an ASP.NET Core backend and an Angular front-end. It covers data models, storage patterns, indexing, querying, validation, typed values, versioning, auditing, reporting strategies, caching, and pitfalls — from beginner to expert.
Goals & constraints
Design goals:
Support dynamic attributes per entity (products, customers, orders) without schema migrations.
Keep reads fast for common access patterns (filtering, sorting, projection).
Maintain data correctness (types, constraints, enumerations).
Provide good auditability and versioning.
Make it easy for Angular UI to render dynamic forms and for .NET to consume typed values.
Support search, reporting, aggregations, and export.
Scale to millions of attribute values.
Constraints & assumptions:
Primary DB is SQL Server (T-SQL), but patterns apply to Postgres/MySQL.
We prefer a mostly relational design (no pure JSONB-only approach), because of reporting and indexing needs.
We will use a hybrid approach — normalized metadata + typed value columns + JSON payload for complex cases.
High-level architecture
Angular UI <--> ASP.NET Core API <--> AVS Engine (SQL Server)
|
+--> Attribute Metadata (catalog)
+--> Attribute Values (typed store)
+--> Index & Materialized Views
+--> Audit + Versioning tables
+--> Background worker (re-indexing, compaction)
Key pieces:
Attribute Catalog — metadata for attributes (name, type, validation, options).
AVS Store — table(s) holding actual attribute values, typed columns for performance.
Index / Projection Tables — optional materialised tables for fast querying / reporting (denormalised).
API Layer — .NET services that expose typed model APIs and perform validation.
UI — Angular dynamic form generator reading catalog and building forms.
Background services — indexing, cleanup, schema migration helpers.
Workflow diagram (high level)
[Admin UI] -- define attribute metadata --> [AttributeCatalog Table]
|
v
[Angular Form] <-- catalog API --------------
|
user fills and submits
|
v
[API] validate using catalog -> write to AVS store -> audit log
|
v
Optional: background job updates materialized projection for reporting
Flowchart: write & read paths
Write:
User submits -> API fetches attribute definitions -> Validate values -> Insert/Update typed AVS rows -> Insert Audit row -> Return success
Read:
UI requests entity + attributes -> API fetches attribute metadata -> API fetches AVS typed values (join) -> API transforms to DTO -> UI renders form
Data model — core tables
We will use a hybrid typed-value model to avoid excessive casting and improve indexing.
1. AttributeCatalog (metadata)
CREATE TABLE AttributeCatalog (
AttributeId BIGINT IDENTITY PRIMARY KEY,
EntityType VARCHAR(100) NOT NULL, -- e.g., 'Product', 'Customer'
AttributeName VARCHAR(200) NOT NULL,
DataType VARCHAR(20) NOT NULL, -- 'int','decimal','string','bool','datetime','json'
IsRequired BIT NOT NULL DEFAULT 0,
IsSearchable BIT NOT NULL DEFAULT 0,
IsSortable BIT NOT NULL DEFAULT 0,
IsMultiValue BIT NOT NULL DEFAULT 0,
AllowedValues NVARCHAR(MAX) NULL, -- JSON array for enums
ValidationRule NVARCHAR(MAX) NULL, -- optional JSON or expression
DefaultValue NVARCHAR(MAX) NULL,
CreatedOn DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
CreatedBy VARCHAR(200) NULL
);
CREATE UNIQUE INDEX UX_AttributeCatalog_Entity_Attr ON AttributeCatalog(EntityType, AttributeName);
2. AttributeValue (typed columns)
We use one row per attribute value (single-valued attributes). For multivalued attributes, multiple rows.
CREATE TABLE AttributeValue (
ValueId BIGINT IDENTITY PRIMARY KEY,
EntityType VARCHAR(100) NOT NULL,
EntityId BIGINT NOT NULL,
AttributeId BIGINT NOT NULL REFERENCES AttributeCatalog(AttributeId),
ValueInt BIGINT NULL,
ValueDecimal DECIMAL(28,8) NULL,
ValueString NVARCHAR(MAX) NULL,
ValueBool BIT NULL,
ValueDateTime DATETIME2 NULL,
ValueJson NVARCHAR(MAX) NULL, -- fallback for complex objects
CreatedOn DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
CreatedBy VARCHAR(200) NULL,
ModifiedOn DATETIME2 NULL,
ModifiedBy VARCHAR(200) NULL,
IsDeleted BIT NOT NULL DEFAULT 0
);
CREATE INDEX IX_AttrVal_EntityAttr ON AttributeValue(EntityType, EntityId, AttributeId);
-- Index searchable typed values:
CREATE INDEX IX_AttrVal_Int ON AttributeValue(AttributeId, ValueInt) WHERE ValueInt IS NOT NULL;
CREATE INDEX IX_AttrVal_Decimal ON AttributeValue(AttributeId, ValueDecimal) WHERE ValueDecimal IS NOT NULL;
CREATE INDEX IX_AttrVal_String ON AttributeValue(AttributeId, ValueString) WHERE ValueString IS NOT NULL;
CREATE INDEX IX_AttrVal_DateTime ON AttributeValue(AttributeId, ValueDateTime) WHERE ValueDateTime IS NOT NULL;
3. AttributeAudit (audit trail)
CREATE TABLE AttributeAudit (
AuditId BIGINT IDENTITY PRIMARY KEY,
ValueId BIGINT NULL,
AttributeId BIGINT NOT NULL,
EntityType VARCHAR(100) NOT NULL,
EntityId BIGINT NOT NULL,
OldValueJson NVARCHAR(MAX) NULL,
NewValueJson NVARCHAR(MAX) NULL,
Action VARCHAR(20) NOT NULL, -- 'INSERT','UPDATE','DELETE'
PerformedBy VARCHAR(200) NULL,
PerformedOn DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
);
CREATE INDEX IX_AttributeAudit_Entity ON AttributeAudit(EntityType, EntityId, PerformedOn);
Why typed columns + fallback JSON?
Many EAV designs use a single Value column (nvarchar(max)). That simplifies the schema but kills indexing, sorting and numeric range queries. By having typed columns:
Numeric and date queries can use native indexes.
Sorting and aggregation are efficient.
We keep ValueJson column as fallback for complex objects (lists, nested structures).
The catalog tells which typed column to use for each attribute.
Read & write flow: details
write (create/update attribute value)
API receives entityId, entityType, and a payload of attributeName → value.
API fetches AttributeCatalog rows for given entityType and attributeNames (cacheable).
For each attribute:
Validate type, required, allowed values, rules.
Map value to correct typed column.
If multi-value attribute, delete existing rows for entity+attribute and insert multiple rows.
Upsert into
AttributeValue(using MERGE or stored proc).Insert into
AttributeAuditwith old vs new JSON payload.
Example MERGE (simplified)
MERGE AttributeValue AS T
USING (VALUES (@EntityType, @EntityId, @AttributeId, @ValueInt, ..., @Now, @User)) AS S(EntityType, EntityId, AttributeId, ValueInt, ValueString, ModifiedOn, ModifiedBy)
ON T.EntityType = S.EntityType AND T.EntityId = S.EntityId AND T.AttributeId = S.AttributeId AND T.IsDeleted = 0
WHEN MATCHED THEN
UPDATE SET ValueInt = S.ValueInt, ..., ModifiedOn = S.ModifiedOn, ModifiedBy = S.ModifiedBy
WHEN NOT MATCHED THEN
INSERT (EntityType, EntityId, AttributeId, ValueInt, ValueString, CreatedOn, CreatedBy) VALUES (S.EntityType, S.EntityId, S.AttributeId, S.ValueInt, S.ValueString, S.ModifiedOn, S.ModifiedBy);
read
API loads attribute catalog for the entityType (from cache).
Join
AttributeValuefiltered byEntityTypeandEntityId, return attributeName → typed value.For Angular, produce a DTO:
{
"entityId": 123,
"attributes": [
{ "attributeName": "Color", "dataType": "string", "value": "Blue" },
{ "attributeName": "Weight", "dataType": "decimal", "value": 10.5 }
]}
Validation & business rules
AttributeCatalog stores ValidationRule and AllowedValues. Validation options:
Type checks — ensure value matches type.
Range checks — min/max for numbers and dates.
Regex — for strings (emails, phone).
Dependency rules — if attribute A = X then B must be present.
Custom rule script — stored as safe expression (JsonLogic, CEL) and executed in sandbox at API level. Avoid storing raw T-SQL for safety.
Implement validation in .NET

Join the conversation! Your thoughts help the community grow.