Runtime tracking
Typed generated contract
public record Album(int Id, string Title);
public interface IAlbumEdit : IRuntimeTrackingItem<Album>
{
int Id { get; }
string Title { get; set; }
}
Album original = new(12, "Blue");
RuntimeTrackingOptions<Album> options = RuntimeTracking.CreateOptions<Album, IAlbumEdit>();
RuntimeTrackingRegistration<Album, IAlbumEdit> registration = options.GetRegistration<IAlbumEdit>();
IAlbumEdit edit = registration.Create(original);
edit.Title = "Kind of Blue";
The generated CLR type implements the requested interface.
Runtime member surface
IRuntimeTrackingItem<Album> edit = RuntimeTracking.Default<Album>().Create(original);
edit.Set(nameof(Album.Title), "Kind of Blue");
string title = edit.Get<string>(nameof(Album.Title));
The default registration is shared for the original type.
Custom options
RuntimeTrackingOptions<Album> options = RuntimeTracking.CreateOptions<Album>();
options.Member<string>(nameof(Album.Title));
IRuntimeTrackingItem<Album> edit = options.GetRegistration<IRuntimeTrackingItem<Album>>().Create(original);
Options freeze when the first registration is created.
RuntimeTrackingOptions<Album> options = RuntimeTracking.CreateOptions<Album>();
options.Member<string>(nameof(Album.Title)).ReadOnly();
RuntimeTrackingRegistration<Album, IRuntimeTrackingItem<Album>> registration = options.GetRegistration<IRuntimeTrackingItem<Album>>();
Member surface
RuntimeTrackingOptions<Album> options = RuntimeTracking.CreateOptions<Album>();
options.Member<int>(nameof(Album.Id)).ReadOnly();
options.Member<string>(nameof(Album.Title)).Expose();
options.Member<string>("DisplayText").Ignore();
options.Member<string>("InternalText").RuntimeAccess(false);
options.Member<string>("DisplayText").Parameters(false);
ReadOnly, Expose, Ignore, RuntimeAccess, and Parameters change different generated surfaces of the same member configuration.
Apply another contract
RuntimeTrackingOptions<Album> options = RuntimeTracking.CreateOptions<Album>();
options.Apply<IAlbumEdit>();
IAlbumEdit edit = options.GetRegistration<IAlbumEdit>().Create(original);
Apply<TContract>() adds the interface contract to the same option tree before generation.
Runtime only storage
RuntimeTrackingOptions<Album> options = RuntimeTracking.CreateOptions<Album>();
options.Member<string>("SelectionState").Direct();
options.Member<string>("DraftNote").SnapshotValue();
Direct() stores the value on the generated item. SnapshotValue() stores its accepted value on the generated item and moves edits into the lazy snapshot. Both members are independent from the original Album.
The same configuration can be declared on generated contract members with RuntimeDirectAttribute, RuntimeSnapshotValueAttribute, and RuntimeValueAttribute. Read and write projection can be bound separately with ReadFromAttribute and WriteToAttribute. IncludeOriginalMembersAttribute controls original member inclusion from a contract.
Attribute configuration
RuntimeReadOnlyAttribute · RuntimeIgnoreAttribute · NestedEditAttribute
BindToAttribute · ReadFromAttribute · WriteToAttribute · ReadWithAttribute · WriteWithAttribute
RuntimeDynamicAccessAttribute · NoRuntimeAccessAttribute
RuntimeParameterAttribute · RuntimeParameterNameAttribute · RuntimeParameterAliasAttribute · RuntimeParametersAttribute
Attributes can configure the generated contract directly.
public sealed class ConfiguredAlbum
{
public int Id { get; set; }
public string Title { get; set; } = "";
}
public interface IConfiguredAlbumEdit : IRuntimeTrackingItem<ConfiguredAlbum>
{
[RuntimeReadOnly]
int Id { get; }
[RuntimeIgnore]
string? DebugLabel { get; }
string Title { get; set; }
}
Query parameter projection
static readonly QueryCommand UpdateAlbum = new("UPDATE albums SET Title = @Title WHERE AlbumId = @Id");
RuntimeTrackingOptions<Album> typedOptions = RuntimeTracking.CreateOptions<Album, IAlbumEdit>();
RuntimeTrackingRegistration<Album, IAlbumEdit> typedRegistration = typedOptions.GetRegistration<IAlbumEdit>();
IAlbumEdit edit = typedRegistration.Create(original);
edit.Title = "Kind of Blue";
UpdateAlbum.Execute(cnn, edit);
Parameters(false) removes a configured member from this parameter projection.
New original factory
public sealed class AlbumDraft
{
public int Id { get; set; }
public string Title { get; set; } = "";
}
RuntimeTrackingOptions<AlbumDraft> options = RuntimeTracking.CreateOptions<AlbumDraft>();
options.WithNewOriginal(static () => new AlbumDraft());
RuntimeTrackingRegistration<AlbumDraft, IRuntimeTrackingItem<AlbumDraft>> registration = options.GetRegistration<IRuntimeTrackingItem<AlbumDraft>>();
IRuntimeTrackingItem<AlbumDraft> edit = registration.CreateNew();
if (registration.CanCreateNew)
registration.CreateNew();
Missing original represented by null
RuntimeTrackingOptions<Album?> options = RuntimeTracking.CreateOptions<Album?>();
options.UseNullAsMissingOriginal();
IRuntimeTrackingItem<Album?> edit = options.GetRegistration<IRuntimeTrackingItem<Album?>>().Create(null);
if (edit.TryGetOriginal(out Album? accepted))
Console.WriteLine(accepted.Title);
Nested edit in place
public record Artist(int Id, string Name);
public record Album(int Id, string Title, Artist Artist);
RuntimeTrackingOptions<Album> options = RuntimeTracking.CreateOptions<Album>();
options.Member<Artist>(nameof(Album.Artist)).NestedEdit(NestedEditMode.InPlace);
Confirming an in-place nested edit copies changed nested members into the accepted nested object.
public sealed class Address
{
public string City { get; set; } = "";
}
public sealed class Contact
{
public Address Address { get; set; } = new();
}
public interface IContactEdit : IRuntimeTrackingItem<Contact>
{
Address Address { get; }
}
Contact original = new() { Address = new() { City = "Toronto" } };
RuntimeTrackingOptions<Contact> contactOptions = RuntimeTracking.CreateOptions<Contact>();
contactOptions.Member<Address>(nameof(Contact.Address)).NestedEdit(NestedEditMode.InPlace);
IContactEdit edit = contactOptions.GetRegistration<IContactEdit>().Create(original);
IEditable editable = (IEditable)edit;
editable.EnsureEditing();
edit.Address.City = "Montreal";
editable.ConfirmEdit();
// InPlace keeps the accepted Address instance and applies the changed City to it.
Nested edit replacement
options.Member<Artist>(nameof(Album.Artist)).NestedEdit(NestedEditMode.Replacement);
Confirming replacement assigns the accepted nested value from the edited nested value instead.
public sealed class ReplacementAddress
{
public string City { get; set; } = "";
}
public sealed class ReplacementContact
{
public ReplacementAddress Address { get; set; } = new();
}
public interface IReplacementContactEdit : IRuntimeTrackingItem<ReplacementContact>
{
ReplacementAddress Address { get; }
}
ReplacementContact original = new() { Address = new() { City = "Toronto" } };
RuntimeTrackingOptions<ReplacementContact> replacementOptions = RuntimeTracking.CreateOptions<ReplacementContact>();
replacementOptions.Member<ReplacementAddress>(nameof(ReplacementContact.Address)).NestedEdit(NestedEditMode.Replacement);
IReplacementContactEdit edit = replacementOptions.GetRegistration<IReplacementContactEdit>().Create(original);
IEditable editable = (IEditable)edit;
editable.EnsureEditing();
edit.Address.City = "Montreal";
editable.ConfirmEdit();
// Replacement assigns the edited Address instance to original.Address.
The two modes change confirmation behavior. The nested edit state is tracked in both forms.
Materialize a list contract
List<Album> source = cnn.Query<List<Album>>("SELECT AlbumId AS Id, Title FROM albums ORDER BY AlbumId");
TrackingList<IAlbumEdit> albums = source.ToTrackingList<Album, IAlbumEdit>();
albums[0].Title = "Kind of Blue";
TrackingList<IAlbumEdit> albums = source.ToTrackingList<Album, IAlbumEdit>(options);