Core EAV
The Entity-Attribute-Value (EAV) framework is the data model underlying every business concept in the trading platform: orders, positions, scenarios, contracts, accounts. Every "thing" in the system is an entity with named attributes whose values are produced by pluggable providers.
The core types
classDiagram
class IEntity~V, I~ {
<<interface>>
+Name: string
+Identifier: I
+Attributes: IAttribute~V~[]
}
class IAttribute~V~ {
<<interface>>
+Name: string
+Type: Type
}
class IValueProvider~V, I~ {
<<interface>>
+GetValue(entity: IEntity~V,I~) V?
}
class IRegistry {
<<interface>>
+Register(entity)
+AttachValueProvider(entityId, attrName, provider)
+GetValue~V~(entityId) V?
}
class Registry {
-entities: ConcurrentDictionary
-providers: ConcurrentDictionary
}
IEntity --> IAttribute
IAttribute --> IValueProvider
IValueProvider --> IRegistry
Registry ..|> IRegistry
IEntity<V, I>
Represents a typed entity. V is the value type, I is the identifier type. An entity has a name, an identifier, and a list of attributes.
public interface IEntity<V, I> : IEvent
where V : struct
where I : IEntityIdentifier
{
string Name { get; }
I Identifier { get; }
IEnumerable<IAttribute<V>> Attributes { get; }
}
IAttribute<V>
A named, typed attribute. The Type property returns the CLR type that the value provider produces.
public interface IAttribute<V> : IEvent
where V : struct
{
string Name { get; }
Type Type { get; }
}
IValueProvider<V, I>
A function from entity → optional value. Value providers are pluggable — an attribute's value can come from a static config, a market data feed, or a derived computation.
public interface IValueProvider<V, I>
where V : struct
where I : IEntityIdentifier
{
V? GetValue(IEntity<V, I> entity);
}
IRegistry (and the concrete Registry)
The global registry. Services register entities and value providers at startup; consumers query the registry for current values.
public interface IRegistry
{
void Register<V, I>(IEntity<V, I> entity) where V : struct where I : IEntityIdentifier;
void AttachValueProvider<V, I>(IIdentifier id, string attributeName, IValueProvider<V, I> provider) where V : struct where I : IEntityIdentifier;
V? GetValue<V>(IIdentifier id) where V : struct;
IEnumerable<V> GetAllValues<V>(IIdentifier id) where V : struct;
}
Usage
using Virtufin.Core;
var registry = new Registry();
// Register an entity: "BTC price"
var entityId = new EntityIdentifier<MarketEntity>("btc-price");
var entity = new MarketEntity(entityId, "BTC Spot Price", new[]
{
new Attribute<DecimalAmount>("last-trade"),
new Attribute<DecimalAmount>("24h-volume"),
});
registry.Register(entity);
// Attach value providers
registry.AttachValueProvider(entityId, "last-trade", () => currentBtcPrice);
registry.AttachValueProvider(entityId, "24h-volume", () => current24hVolume);
// Query
var lastTrade = registry.GetValue<DecimalAmount>(entityId);
Why EAV?
The trading platform's domain has many "kinds of things" (orders, positions, scenarios, contracts, accounts, ...). A traditional ORM would model each as a separate class. The EAV model instead:
- Unifies the access pattern: every "thing" is an entity, looked up by name.
- Allows dynamic schemas: new attributes can be added without changing the class hierarchy.
- Enables attribute-level caching: the registry caches per-attribute, per-entity values; if a
last-tradevalue hasn't changed, downstream consumers don't recompute. - Decouples computation from declaration: a value provider can be a static config today and a market data feed tomorrow, with no change to the entity declaration.
The cost is that the CLR can't statically enforce that "an order has a quantity attribute" — that's a runtime check. We accept this trade-off for the flexibility.
Generics summary
| Type | Constraints | Purpose |
|---|---|---|
IEntity<V, I> |
V : struct, I : IEntityIdentifier |
A typed entity |
IAttribute<V> |
V : struct |
A named, typed attribute |
IValueProvider<V, I> |
V : struct, I : IEntityIdentifier |
A function from entity → value |
IRegistry |
(none) | Global registry interface |
Registry |
(none) | Concrete in-memory registry |
V is the value type, I is the identifier type — same convention as the rest of the library.