Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upcasters #2314

Merged
merged 12 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
779 changes: 779 additions & 0 deletions src/EventSourcingTests/SchemaChange/Upcasters.cs

Large diffs are not rendered by default.

82 changes: 52 additions & 30 deletions src/Marten/Events/EventDocumentStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ namespace Marten.Events
/// mapping for the event store in a running system. The actual implementation of this
/// base type is generated and compiled at runtime by Marten
/// </summary>
public abstract class EventDocumentStorage : IEventStorage
public abstract class EventDocumentStorage: IEventStorage
{
private readonly EventQueryMapping _mapping;
private readonly ISerializer _serializer;
Expand Down Expand Up @@ -103,6 +103,7 @@ public IDeletion HardDeleteForDocument(IEvent document, string tenantId)

public string FromObject { get; }
public Type SelectedType => typeof(IEvent);

public void WriteSelectClause(CommandBuilder sql)
{
sql.Append(_selectClause);
Expand All @@ -118,7 +119,8 @@ public ISelector BuildSelector(IMartenSession session)
return this;
}

public IQueryHandler<T> BuildHandler<T>(IMartenSession session, Statement topStatement, Statement currentStatement)
public IQueryHandler<T> BuildHandler<T>(IMartenSession session, Statement topStatement,
Statement currentStatement)
{
return LinqHandlerBuilder.BuildHandler<IEvent, T>(this, topStatement);
}
Expand All @@ -130,6 +132,7 @@ public ISelectClause UseStatistics(QueryStatistics statistics)

public Type SourceType => typeof(IEvent);
public IFieldMapping Fields { get; }

public ISqlFragment FilterDocuments(QueryModel model, ISqlFragment query)
{
var shouldBeTenanted = Events.TenancyStyle == TenancyStyle.Conjoined && !query.SpecifiesTenant();
Expand Down Expand Up @@ -157,10 +160,11 @@ public ISqlFragment DefaultWhereFragment()

public object IdentityFor(IEvent document)
{
return Events.StreamIdentity == StreamIdentity.AsGuid ? (object) document.Id : document.StreamKey;
return Events.StreamIdentity == StreamIdentity.AsGuid ? (object)document.Id : document.StreamKey;
}

public Type IdType { get; }

public Guid? VersionFor(IEvent document, IMartenSession session)
{
return null;
Expand Down Expand Up @@ -201,7 +205,9 @@ public IStorageOperation Overwrite(IEvent document, IMartenSession session, stri
throw new NotSupportedException();
}

public abstract IStorageOperation AppendEvent(EventGraph events, IMartenSession session, StreamAction stream, IEvent e);
public abstract IStorageOperation AppendEvent(EventGraph events, IMartenSession session, StreamAction stream,
IEvent e);

public abstract IStorageOperation InsertStream(StreamAction stream);
public abstract IQueryHandler<StreamState> QueryForStream(StreamAction stream);
public abstract IStorageOperation UpdateStreamVersion(StreamAction stream);
Expand All @@ -213,18 +219,11 @@ public IEvent Resolve(DbDataReader reader)
if (mapping == null)
{
var dotnetTypeName = reader.GetFieldValue<string>(2);
if (dotnetTypeName.IsEmpty())
{
throw new UnknownEventTypeException(eventTypeName);
}

var type = Events.TypeForDotNetName(dotnetTypeName);
mapping = Events.EventMappingFor(type);
mapping = eventMappingForDotNetTypeName(dotnetTypeName, eventTypeName);
}

var data = _serializer.FromJson(mapping.DocumentType, reader, 0).As<object>();

var @event = mapping.Wrap(data);
var @event = deserializeEvent(mapping, reader);

ApplyReaderDataToEvent(reader, @event);

Expand All @@ -240,25 +239,11 @@ public async Task<IEvent> ResolveAsync(DbDataReader reader, CancellationToken to
if (mapping == null)
{
var dotnetTypeName = await reader.GetFieldValueAsync<string>(2, token).ConfigureAwait(false);
if (dotnetTypeName.IsEmpty())
{
throw new UnknownEventTypeException(eventTypeName);
}
Type type;
try
{
type = Events.TypeForDotNetName(dotnetTypeName);
}
catch (ArgumentNullException)
{
throw new UnknownEventTypeException(dotnetTypeName);
}
mapping = Events.EventMappingFor(type);
}

var data = await _serializer.FromJsonAsync(mapping.DocumentType, reader, 0, token).ConfigureAwait(false);
mapping = eventMappingForDotNetTypeName(dotnetTypeName, eventTypeName);
}

var @event = mapping.Wrap(data);
var @event = await deserializeEventAsync(mapping, reader, token).ConfigureAwait(false);

await ApplyReaderDataToEventAsync(reader, @event, token).ConfigureAwait(false);

Expand All @@ -267,6 +252,43 @@ public async Task<IEvent> ResolveAsync(DbDataReader reader, CancellationToken to

public abstract Task ApplyReaderDataToEventAsync(DbDataReader reader, IEvent e, CancellationToken token);

private EventMapping eventMappingForDotNetTypeName(string dotnetTypeName, string eventTypeName)
{
if (dotnetTypeName.IsEmpty())
{
throw new UnknownEventTypeException(eventTypeName);
}

Type type;
try
{
type = Events.TypeForDotNetName(dotnetTypeName);
}
catch (ArgumentNullException)
{
throw new UnknownEventTypeException(dotnetTypeName);
}

return Events.EventMappingFor(type);
}

private IEvent deserializeEvent(EventMapping mapping, DbDataReader reader)
{
var data = mapping.Transformation != null?
mapping.Transformation.FromDbDataReader(_serializer, reader, 0)
: _serializer.FromJson(mapping.DocumentType, reader, 0);

return mapping.Wrap(data);
}

private async ValueTask<IEvent> deserializeEventAsync(EventMapping mapping, DbDataReader reader,
CancellationToken token)
{
var data = mapping.Transformation != null ?
await mapping.Transformation.FromDbDataReaderAsync(_serializer, reader, 0, token).ConfigureAwait(false)
: await _serializer.FromJsonAsync(mapping.DocumentType, reader, 0, token).ConfigureAwait(false);

return mapping.Wrap(data);
}
}
}
88 changes: 83 additions & 5 deletions src/Marten/Events/EventGraph.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Baseline;
using Baseline.ImTools;
using Marten.Events.Daemon;
using Marten.Events.Operations;
using Marten.Events.Projections;
using Marten.Events.Schema;
using Marten.Exceptions;
using Marten.Internal;
using Marten.Services.Json.Transformations;
using Marten.Storage;
using Marten.Util;
using NpgsqlTypes;
using Weasel.Core;
using Weasel.Postgresql;

namespace Marten.Events
{
Expand All @@ -30,7 +31,6 @@ public partial class EventGraph: IEventStoreOptions, IReadOnlyEventStoreOptions

private readonly Lazy<IProjection[]> _inlineProjections;


private readonly Ref<ImHashMap<string, Type>> _nameToType = Ref.Of(ImHashMap<string, Type>.Empty);

private string _databaseSchemaName;
Expand Down Expand Up @@ -130,6 +130,86 @@ public void MapEventType(Type eventType, string eventTypeName)
eventMapping.EventTypeName = eventTypeName;
}

public IEventStoreOptions Upcast<TEvent>(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeremydmiller, maybe it would be worth grouping those transformations into nested property instead of adding them straight into EventGraph? E.g. options.Events.Transformations.Upcast(...). Thoughts?

Copy link
Member

@jeremydmiller jeremydmiller Aug 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but that's a lot of dots and not super discoverable. I think I'd leave it on EventGraph, but maybe look at what could be hidden or simplified in v6.

It's not a precise rule by any stretch of the imagination, but I think you try to limit the number of navigation dots or nested closure levels to two in configuration code like this. I don't have a super strong opinion about this though.

string eventTypeName,
JsonTransformation jsonTransformation = null
) where TEvent : class =>
Upcast(typeof(TEvent), eventTypeName, jsonTransformation);

public IEventStoreOptions Upcast(
Type eventType,
string eventTypeName,
JsonTransformation jsonTransformation = null
)
{
var eventMapping = EventMappingFor(eventType);
eventMapping.EventTypeName = eventTypeName;
eventMapping.Transformation = jsonTransformation;

return this;
}

public IEventStoreOptions Upcast<TOldEvent, TEvent>(
string eventTypeName,
Func<TOldEvent, TEvent> upcast
) where TOldEvent : class where TEvent : class
{
var eventMapping = EventMappingFor<TEvent>();
eventMapping.EventTypeName = eventTypeName;
eventMapping.Transformation = JsonTransformations.Upcast(upcast);

return this;
}

public IEventStoreOptions Upcast<TOldEvent, TEvent>(
Func<TOldEvent, TEvent> upcast
) where TOldEvent : class where TEvent : class =>
Upcast(typeof(TOldEvent).GetEventTypeName(), upcast);

public IEventStoreOptions Upcast<TOldEvent, TEvent>(
string eventTypeName,
Func<TOldEvent, CancellationToken, Task<TEvent>> upcastAsync
) where TOldEvent : class where TEvent : class
{
var eventMapping = EventMappingFor<TEvent>();
eventMapping.EventTypeName = eventTypeName;
eventMapping.Transformation = JsonTransformations.Upcast(upcastAsync);

return this;
}

public IEventStoreOptions Upcast<TOldEvent, TEvent>(
Func<TOldEvent, CancellationToken, Task<TEvent>> upcastAsync
) where TOldEvent : class where TEvent : class =>
Upcast(typeof(TOldEvent).GetEventTypeName(), upcastAsync);

public IEventStoreOptions Upcast(params IEventUpcaster[] upcasters)
{
foreach (var upcaster in upcasters)
{
Upcast(
upcaster.EventType,
upcaster.EventTypeName,
new JsonTransformation(upcaster.FromDbDataReader, upcaster.FromDbDataReaderAsync)
);
}

return this;
}

public IEventStoreOptions Upcast<TUpcaster>() where TUpcaster : IEventUpcaster, new()
{
var upcaster = new TUpcaster();

Upcast(
upcaster.EventType,
upcaster.EventTypeName,
new JsonTransformation(upcaster.FromDbDataReader, upcaster.FromDbDataReaderAsync)
);

return this;
}

/// <summary>
/// Override the database schema name for event related tables. By default this
/// is the same schema as the document storage
Expand Down Expand Up @@ -207,7 +287,6 @@ internal string AggregateAliasFor(Type aggregateType)
return alias;
}


internal string GetStreamIdDBType()
{
return StreamIdentity == StreamIdentity.AsGuid ? "uuid" : "varchar";
Expand Down Expand Up @@ -256,7 +335,6 @@ internal IEventStorage EnsureAsGuidStorage(IMartenSession session)
return session.EventStorage();
}


internal IEvent BuildEvent(object eventData)
{
if (eventData == null)
Expand Down
12 changes: 9 additions & 3 deletions src/Marten/Events/EventMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Baseline;
using LamarCodeGeneration;
using Marten.Events.Archiving;
using Marten.Internal;
Expand All @@ -22,6 +21,7 @@
using Weasel.Postgresql;
using Marten.Schema;
using Marten.Services;
using Marten.Services.Json.Transformations;
using Marten.Storage;
using Marten.Util;
using NpgsqlTypes;
Expand Down Expand Up @@ -52,7 +52,7 @@ protected EventMapping(EventGraph parent, Type eventType)
_parent = parent;
DocumentType = eventType;

EventTypeName = eventType.IsGenericType ? eventType.ShortNameInCode() : DocumentType.Name.ToTableAlias();
EventTypeName = eventType.GetEventTypeName();
IdMember = DocumentType.GetProperty(nameof(IEvent.Id));

_inner = new DocumentMapping(eventType, parent.Options);
Expand All @@ -65,14 +65,14 @@ protected EventMapping(EventGraph parent, Type eventType)
filter = filter.CombineAnd(CurrentTenantFilter.Instance);
}


_defaultWhereFragment = filter;
}

Type IEventType.EventType => DocumentType;

public string DotNetTypeName { get; set; }

public JsonTransformation? Transformation { get; set; }

IDocumentMapping IDocumentMapping.Root => this;
public Type DocumentType { get; }
Expand Down Expand Up @@ -301,4 +301,10 @@ public override IEvent Wrap(object data)
};
}
}

internal static class EventMappingExtensions
{
internal static string GetEventTypeName(this Type eventType) =>
eventType.IsGenericType ? eventType.ShortNameInCode() : eventType.Name.ToTableAlias();
}
}
Loading