Dynamic.Json.EfCore.AspNetCore

ASP.NET Core query-string parsing helpers for Dynamic.Json.EfCore search filters.


Keywords
aspnetcore, dynamic-fields, ef-core, json, querying, sqlserver
License
MIT
Install
Install-Package Dynamic.Json.EfCore.AspNetCore -Version 0.1.0-preview.1

Documentation

Dynamic.Json.EfCore

CI Dynamic.Json.Search Dynamic.Json.EfCore Dynamic.Json.EfCore.SqlServer Dynamic.Json.AspNetCore

Introduction

Dynamic.Json.EfCore is a set of .NET packages for building applications with user-defined JSON fields without giving up EF Core mapping, change tracking, validation, or SQL-backed querying.

It is designed for relational applications that need flexible, metadata-driven fields such as custom profile attributes, dynamic forms, tenant-specific fields, configurable records, or admin-defined search filters. Instead of treating JSON columns as opaque blobs, the package set provides typed search parsing, EF Core JSON value conversion, provider-specific SQL translation, and ASP.NET Core query-string adapters.

At its core, Dynamic.Json.EfCore lets developers treat metadata-defined fields as first-class citizens—searchable, validated, and queryable through Entity Framework Core instead of as raw JSON strings.

Is This the Right Fit?

This package set is designed for relational applications where most of the data model is stable, but a subset of fields must be defined at runtime.

Typical scenarios include:

  • HR systems with employee-type-specific fields
  • CRM platforms with tenant-defined contact attributes
  • Workflow applications with dynamic intake forms
  • Asset management systems with configurable metadata
  • SaaS products that allow customers to define searchable custom fields

Consider Dynamic.Json.EfCore if you want to:

  • Store user-defined fields in JSON while keeping the rest of your model relational.
  • Generate forms, validation, and search filters from runtime metadata.
  • Execute filtering in SQL instead of loading records into memory.
  • Continue using Entity Framework Core, SQL Server or PostgreSQL, and strongly typed application code.

It is not intended to replace a document database. If your application is primarily document-oriented or your entire schema is dynamic, a native document database may be a better choice.

Design Philosophy

Dynamic.Json.EfCore is built around the idea that most business applications don't need an entirely dynamic database—they need a relational model with a flexible extension point.

Rather than generating tables and columns at runtime or treating JSON as an opaque blob, the package stores only the dynamic portion of an entity as JSON.

Metadata defines the structure of those fields, enabling validation, search parsing, provider-specific SQL translation, and integration with the rest of your EF Core model.

This approach lets applications retain the strengths of Entity Framework Core—including change tracking, migrations, LINQ integration, and relational performance—while allowing metadata-driven fields to evolve without database schema changes.

Architecture Overview

A typical request flows through the package set like this:

flowchart LR
    subgraph API["API Layer"]
        A["HTTP Query String"]
        B["Dynamic.Json.AspNetCore<br/>Query String Adapter"]
    end

    subgraph Core["Core Packages"]
        C["Dynamic.Json.Search<br/>Search Parser"]
        D["Metadata Definitions"]
        E["Expression Builder"]
    end

    subgraph Persistence["Persistence Layer"]
        F["Entity Framework Core"]
        G["Provider Package<br/>SQL Translation"]
        H["SQL Server JSON<br/>or PostgreSQL jsonb"]
    end

    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
    F --> G
    G --> H
Loading

Incoming query parameters are parsed into strongly typed search criteria, validated against runtime metadata, translated into LINQ expression trees, and finally converted into provider-specific SQL that executes directly against SQL Server JSON columns or PostgreSQL jsonb columns.

Packages

Install the packages for the layers you are building, choosing the SQL Server or PostgreSQL provider package that matches your database:

dotnet add package Dynamic.Json.Search
dotnet add package Dynamic.Json.EfCore

# Choose one database provider:
dotnet add package Dynamic.Json.EfCore.SqlServer
# or
dotnet add package Dynamic.Json.EfCore.PostgreSql

dotnet add package Dynamic.Json.AspNetCore
Package Use it for
Dynamic.Json.Search Provider-neutral dynamic search field/filter models, parser, and parse result/error contracts.
Dynamic.Json.EfCore Provider-neutral EF Core primitives for JSON conversion, value comparison, and EF query marker functions.
Dynamic.Json.EfCore.SqlServer SQL Server translation for provider-neutral JSON query functions such as string, decimal, and date lookups.
Dynamic.Json.EfCore.PostgreSql PostgreSQL jsonb persistence and scalar string, decimal, and date query translation.
Dynamic.Json.AspNetCore ASP.NET Core query-string adapters and service registration for dynamic search parsing.

The current package version is 0.3.0-preview.1 and targets .NET 10.

PostgreSQL provider

Register the PostgreSQL translators on the same options builder used for Npgsql:

services.AddDbContext<AppDbContext>(options =>
    options
        .UseNpgsql(connectionString)
        .UseDynamicJsonPostgreSql());

Map the dynamic JSON property to PostgreSQL jsonb:

modelBuilder.Entity<Employee>()
    .Property(employee => employee.FieldValues)
    .HasColumnType("jsonb")
    .HasJsonConversion();

The provider supports server-side DynamicJsonFunctions.Value, ValueDecimal, and ValueDate queries through the same provider-neutral API used by SQL Server. It targets EF Core >= 10.0.9 and < 11.0.0, Npgsql >= 10.0.3 and < 11.0.0, and PostgreSQL 16 or later; integration tests currently run against PostgreSQL 18.

Generated PostgreSQL SQL uses jsonb_path_query_first; guarded decimal and date conversions use pg_input_is_valid. PostgreSQL stores native jsonb, while SQL Server stores JSON text in nvarchar(max) and extracts scalars with JSON_VALUE. Both providers preserve the same path, missing-value, JSON-null, and safe-conversion contract.

See the PostgreSQL provider guide for installation, scalar query examples, generated SQL illustrations, compatibility policy, and provider differences.

Quick Start

Map a JsonObject property with EF Core:

using System.Text.Json.Nodes;
using Dynamic.Json.EfCore.Metadata;

public sealed class Employee
{
    public int Id { get; set; }
    public JsonObject FieldValues { get; set; } = new();
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>(entity =>
    {
        entity.Property(e => e.FieldValues).HasJsonConversion();
    });
}

Enable SQL Server JSON translation:

using Dynamic.Json.EfCore.SqlServer;

options.UseSqlServer(connectionString)
    .UseDynamicJsonSqlServer();

Query JSON values through provider-neutral marker functions:

using Dynamic.Json.EfCore.Querying;

var seniorEmployees = await db.Employees
    .Where(employee =>
        DynamicJsonFunctions.Value(employee.FieldValues, "$.certificationLevel") == "senior")
    .ToListAsync();

Parse dynamic search parameters in an application or API layer:

using Dynamic.Json.Search;

var fields = new[]
{
    new DynamicSearchField("certificationLevel", DynamicSearchFieldType.Select, new[] { "junior", "senior" }),
    new DynamicSearchField("hourlyRate", DynamicSearchFieldType.Number),
    new DynamicSearchField("remoteEligible", DynamicSearchFieldType.Boolean),
};

var parser = new DynamicSearchQueryParser();
var result = parser.Parse(
    new Dictionary<string, string?>
    {
        ["certificationLevel"] = "senior",
        ["hourlyRate_gte"] = "75",
        ["remoteEligible"] = "true",
    },
    fields);

if (result.Errors.Count > 0)
{
    // Return structured validation errors to the caller.
}

ASP.NET Core apps can register parser services and parse IQueryCollection directly:

using Dynamic.Json.AspNetCore;

builder.Services.AddDynamicJsonAspNetCore();

var result = parser.Parse(Request.Query, fields);

Real-World Example

For a full application built around these packages, see Dynamic HR Demo. It is a metadata-driven employee record system that uses Dynamic.Json for runtime-defined fields, dynamic validation, JSON persistence, SQL Server filtering, clean architecture boundaries, and an end-to-end React UI.

Engineering Highlights

  • Package boundaries keep search parsing, ASP.NET Core adapters, EF Core mapping, and provider SQL translation separate.
  • The search parser is provider-neutral, so application services can validate filters without referencing ASP.NET Core, EF Core, or a database provider.
  • SQL Server and PostgreSQL translations use EF Core provider expression APIs rather than raw SQL string concatenation.
  • Dynamic field names, operators, number/date/boolean values, and select options are validated before query translation.
  • JSON value comparison supports semantic and serialized modes for different correctness/performance tradeoffs.
  • Unit tests cover provider-neutral behavior; Docker/Testcontainers integration tests verify SQL Server and PostgreSQL persistence and generated SQL.
  • CI builds, tests, packs, runs vulnerability checks, publishes coverage summaries, and runs provider integration tests.

Package Architecture

The package set is intentionally split so applications only reference the layers they need.

flowchart TB
    App["Application"]

    Search["Dynamic.Json.Search"]
    Asp["Dynamic.Json.AspNetCore"]
    Ef["Dynamic.Json.EfCore"]
    Sql["Dynamic.Json.EfCore.SqlServer"]
    Pg["Dynamic.Json.EfCore.PostgreSql"]

    App --> Search
    App --> Asp
    App --> Ef

    Asp --> Search
    Ef --> Search
    Sql --> Ef
    Pg --> Ef
Loading

Package Responsibilities

  • Dynamic.Json.Search owns the provider-neutral search language, metadata models, parser, validation, and parse results. These concepts are independent of EF Core or ASP.NET Core and can be used by applications, workers, tests, or other entry points.

  • Dynamic.Json.AspNetCore adapts IQueryCollection into the provider-neutral parser input and registers ASP.NET Core services. It contains no business validation or database-specific behavior.

  • Dynamic.Json.EfCore provides provider-neutral EF Core primitives including JSON mapping, value comparison, and LINQ marker functions.

  • Dynamic.Json.EfCore.SqlServer translates those provider-neutral marker functions into SQL Server expressions such as JSON_VALUE and TRY_CONVERT, keeping SQL Server implementation details isolated from the rest of the package set.

  • Dynamic.Json.EfCore.PostgreSql maps JsonObject values to native jsonb columns and translates provider-neutral marker functions with PostgreSQL JSON path and conversion expressions.

Documentation

Repository Layout

Dynamic.Json.Search/                  Provider-neutral dynamic search filter models and parser
Dynamic.Json.EfCore/                  Provider-neutral JSON mapping, tracking, and query markers
Dynamic.Json.AspNetCore/              ASP.NET Core dynamic search query adapters
Dynamic.Json.EfCore.SqlServer/        SQL Server EF Core JSON query translations
Dynamic.Json.EfCore.PostgreSql/       PostgreSQL EF Core jsonb persistence and query translations
Dynamic.Json.EfCore.UnitTests/        Unit tests for the package set
Dynamic.Json.EfCore.IntegrationTests/ Docker/Testcontainers-backed provider integration tests
docs/                                 Package documentation and test coverage notes
TODO.md                               Follow-up work and publishing checklist

JSON Mapping and Change Tracking

HasJsonConversion() configures a JsonObject property to be stored as serialized JSON and tracked deeply by EF Core:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>(entity =>
    {
        entity.Property(e => e.FieldValues).HasJsonConversion();
    });
}

By default, HasJsonConversion() uses JsonObjectComparisonMode.Semantic:

entity.Property(e => e.FieldValues)
    .HasJsonConversion(JsonObjectComparisonMode.Semantic);

Semantic comparison treats JSON objects as structured data rather than serialized text:

  • Object property order does not affect equality.
  • Nested objects are compared recursively.
  • Arrays are compared in order, so array ordering remains significant.
  • null equals null, but null does not equal a populated JSON object.
  • null hashes to 0.

These objects are considered equal because they contain the same properties and values:

{ "name": "Jimmy", "occupation": "Lawyer" }
{ "occupation": "Lawyer", "name": "Jimmy" }

Arrays remain order-sensitive, so these arrays are not equal:

["Kim", "Jimmy"]
["Jimmy", "Kim"]

For applications that prefer faster, property-order-sensitive comparison, use JsonObjectComparisonMode.Serialized:

entity.Property(e => e.FieldValues)
    .HasJsonConversion(JsonObjectComparisonMode.Serialized);

Use semantic comparison when JSON object property order should not matter. Use serialized comparison when raw comparison speed is more important and callers are comfortable with property-order-sensitive change detection.

Dynamic Search

The provider-neutral parser converts key/value pairs into typed dynamic filters. For example:

favoriteSongName_contains=Go
numberOfSongs_gte=7
hasIcePowers=true

The parser validates:

  • supported field names
  • supported operators for each field type
  • number, date, boolean, and select-option values
  • ignored framework/application query parameters such as paging keys

Errors are returned as structured parse errors with stable error codes, allowing API consumers to format or localize messages without relying on exception text.

ASP.NET Core applications can use Dynamic.Json.AspNetCore to adapt IQueryCollection into the provider-neutral parser. Non-HTTP applications can pass dictionaries or other simple key/value inputs directly to Dynamic.Json.Search.

Provider Translation

Both relational provider packages translate the same provider-neutral marker functions:

DynamicJsonFunctions.Value(employee.FieldValues, "$.favoriteSongName")
DynamicJsonFunctions.ValueDecimal(employee.FieldValues, "$.numberOfSongs")
DynamicJsonFunctions.ValueDate(employee.FieldValues, "$.coronationDate")

PostgreSQL

PostgreSQL applications enable jsonb persistence and scalar translation with:

using Dynamic.Json.EfCore.PostgreSql;

options.UseNpgsql(connectionString)
    .UseDynamicJsonPostgreSql();

PostgreSQL translation uses jsonb_path_query_first for portable property paths and guards numeric/date casts with pg_input_is_valid, so missing, JSON-null, database-null, and invalid conversion values produce SQL NULL.

Captured paths and comparison values remain EF Core parameters. Constant paths are validated against the portable scalar JSON path contract before SQL generation.

PostgreSQL 16 or later is required because guarded numeric and date translation depends on pg_input_is_valid. Integration tests currently run against PostgreSQL 18.

SQL Server

The SQL Server package plugs into EF Core through:

using Dynamic.Json.EfCore.SqlServer;

options.UseSqlServer(connectionString)
    .UseDynamicJsonSqlServer();

The translator uses EF Core SQL expression APIs instead of raw SQL string concatenation. Store type fragments used by TRY_CONVERT are fixed internally, and user values are kept in EF expression translation.

The SQL Server integration tests exercise this behavior against a real SQL Server 2022 container. They verify that JsonObject values persist and reload through HasJsonConversion(), string lookups translate through JSON_VALUE, numeric lookups use TRY_CONVERT(decimal(18, 4), JSON_VALUE(...)), date lookups use TRY_CONVERT(date, JSON_VALUE(...)), and generated SQL contains the expected SQL Server JSON functions.

Security Notes

  • Dynamic JSON search uses EF Core expression translation rather than raw SQL construction.
  • Dynamic field names are validated before they are converted into JSON paths.
  • LIKE patterns escape wildcard characters before filtering.
  • Package vulnerability checks run in CI:
dotnet list Dynamic.Json.EfCore.slnx package --vulnerable --include-transitive

Build and Test

Build the package solution:

dotnet build Dynamic.Json.EfCore.slnx

Run unit tests:

dotnet test Dynamic.Json.EfCore.UnitTests\Dynamic.Json.EfCore.UnitTests.csproj

Run provider integration tests:

dotnet test Dynamic.Json.EfCore.IntegrationTests\Dynamic.Json.EfCore.IntegrationTests.csproj

The integration tests use Testcontainers.MsSql and Testcontainers.PostgreSql and require Docker to be running. The first run may take longer while Docker pulls the SQL Server 2022 and PostgreSQL 18 images. Tests isolate their records or databases so shared provider containers do not leak state between assertions.

Collect coverage:

dotnet test Dynamic.Json.EfCore.UnitTests\Dynamic.Json.EfCore.UnitTests.csproj --settings coverlet.runsettings --results-directory artifacts\coverage\raw --collect "XPlat Code Coverage"

CI generates an HTML/Cobertura coverage report from the unit test suite, publishes the Markdown summary to the GitHub Actions job summary, uploads the full report as a coverage-report artifact, packs the NuGet packages, and runs SQL Server and PostgreSQL integration tests on ubuntu-latest.

Coverage notes for the package set live in docs/test-coverage.md.

Roadmap

Near-term follow-up work is tracked in TODO.md, including:

  • JsonArray support
  • Future Newtonsoft/JObject support.
  • Swagger/OpenAPI documentation after selecting a package version without known vulnerabilities.