Property Handler: Stop Parsing That JSON Column By Hand
Not every column maps cleanly to a property. A JSON column needs to become a real object. A stored byte[] needs to become a Guid. A stubborn DateTime.Kind needs fixing on every read. RepoDB solves all three with one interface: IPropertyHandler.
The problem: the type mapper has a ceiling
RepoDB’s default type mapping covers strings, numbers, dates, and enums. It has no way to know a NVARCHAR(MAX) column is really a serialized Address, or a byte[] column is really a Guid. Without help, that column stays a raw string. Every caller ends up parsing it by hand.
The property handler, built in
IPropertyHandler<TInput, TResult> has two methods:
Get(TInput input, PropertyHandlerGetOptions options)— runs when a row comes back from the database, converting the column value into the property type.Set(TResult input, PropertyHandlerSetOptions options)— runs when a value goes into the database, converting the property back into the column type.
TInput is the column’s type. TResult is the property’s type. Get turns one into the other on the way in; Set reverses it on the way out.
Writing a handler
A Person table stores Address as NVARCHAR(MAX). The model needs a real Address property.
public class Address
{
public int HouseNo { get; set; }
public string Country { get; set; }
public string State { get; set; }
public string Street { get; set; }
public string Region { get; set; }
public int ZipCode { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class AddressPropertyHandler : IPropertyHandler<string, Address>
{
public Address Get(string input, PropertyHandlerGetOptions options) =>
JsonConvert.DeserializeObject<Address>(input);
public string Set(Address input, PropertyHandlerSetOptions options) =>
JsonConvert.SerializeObject(input);
}
That is the entire conversion. No manual parsing left at any call site.
Attaching it to a property
The simplest way to wire a handler: the PropertyHandler attribute on the property.
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
[PropertyHandler(typeof(AddressPropertyHandler))]
public Address Address { get; set; }
}
From here, every fetch and every write runs through the handler automatically.
using (var connection = new SqlConnection(connectionString))
{
var person = connection.Query<Person>(10045);
Console.WriteLine($"{person.Name}: {person.Address.Street}, {person.Address.Country}");
}
Query returns a Person with Address fully populated. The handler does the conversion. Nothing else has to.
Attaching it without touching the model
Register the same handler from the outside, with PropertyHandlerMapper or FluentMapper.
PropertyHandlerMapper
.Add<Person, AddressPropertyHandler>(e => e.Address, true);
FluentMapper
.Entity<Person>()
.PropertyHandler<AddressPropertyHandler>(e => e.Address, true);
Same effect as the attribute. Registered at startup, not on the class.
Applying it across every property of a type
Some conversions apply to a whole CLR type, not one property. Normalizing every DateTime to UTC is a common example.
public class DateTimeKindToUtcPropertyHandler : IPropertyHandler<DateTime?, DateTime?>
{
public DateTime? Get(DateTime? input, PropertyHandlerGetOptions options) =>
input.HasValue ? DateTime.SpecifyKind(input.Value, DateTimeKind.Utc) : null;
public DateTime? Set(DateTime? input, PropertyHandlerSetOptions options) =>
input.HasValue ? DateTime.SpecifyKind(input.Value, DateTimeKind.Unspecified) : null;
}
PropertyHandlerMapper
.Add<DateTime, DateTimeKindToUtcPropertyHandler>(true);
Register it once. Every DateTime property, on every entity, gets the same treatment.
Good candidates for this pattern
- A JSON or XML column that should surface as a real object.
- A provider-specific type gap, like
Guidmeeting Oracle’sRAW(16). - Enum columns stored as text, mapped back to a real enum.
- Encrypted or encoded columns, decoded on read and encoded on write.
- A value like
DateTime.Kind, normalized the same way everywhere.
Things to keep in mind
- A property handler overrides
TypeMapperand enum conversion. No partial handoff. - Type-level registration is global for the whole process. Scope it to one property when it shouldn’t apply everywhere.
GetandSetrun per property, per row. Keep them cheap.
Learn more
See IPropertyHandler for the interface, PropertyHandler for the attribute, and PropertyHandlerMapper for mapping without touching the model.
Please support us
Star our Github repository |
Follow me at X / Twitter |
Connect with us at our official Teams Channel
Apache License 2.0 — Copyright © 2026 by Michael Camara Pendon / Create a Request
~ Conceptualized and written by me. Reviewed and checked by AI. Refined and gatekept by me. ~