Property Handler: Stop Parsing That JSON Column By Hand
Not every column maps cleanly to a property especially if those columns contains complex data.
For example, if one needs to hydrate:
- A JSON column to become a real object.
- A stored
byte[]to become aGuid. - A
DateTime.Kindto be UTC on every read.
RepoDB can solve all of them with IPropertyHandler.
The problem: the type mapper has a ceiling
RepoDB’s default type mapping covers strings, numbers, dates, enums and other known types.
But like with many other libraries, it has no way to know that an NVARCHAR(MAX) column is a serialized Address, or a byte[] column is a Guid.
Without a help, that column stays a raw string in value and force every caller to parse it by hand.
The property handler, built in
IPropertyHandler<TInput, TResult> has two methods:
Get(TInput input, PropertyHandlerGetOptions options)— this runs when a row comes back from the database, converting the column value into the property type.Set(TResult input, PropertyHandlerSetOptions options)— this runs when a value goes into the database, converting the property back into the column type.
The TInput is the type of the column. The TResult is the type of the class property.
The Get turns data into an object on way in, and the Set reverses it on the way out.
Writing a handler
Suppose a use-case is to convert the Person table that stores Address column as NVARCHAR(MAX) into the model Address property as an object.
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);
}
Above is the entire conversion. Everything happens within that small property handler.
Attaching it to a property
The simplest way to wire it is to use 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);
Applying it across every property of a type
In some cases, conversions must be applied to a whole CLR type, not just one property. Like normalizing every DateTime to UTC.
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);
Just register it once and then every DateTime property, on every entity, gets the same behavior.
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. ~