Skip to content

Commit

Permalink
Isolate type mapper from service result (#369)
Browse files Browse the repository at this point in the history
* Add type mapper interface
* Don't throw in services if the type isn't mapped
  • Loading branch information
alexeyzimarev authored Aug 15, 2024
1 parent 3969b05 commit e5a8461
Show file tree
Hide file tree
Showing 35 changed files with 358 additions and 227 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public abstract partial class CommandService<TAggregate, TState, TId>(
IEventWriter? writer,
AggregateFactoryRegistry? factoryRegistry = null,
StreamNameMap? streamNameMap = null,
TypeMapper? typeMap = null,
ITypeMapper? typeMap = null,
AmendEvent? amendEvent = null
)
: ICommandService<TAggregate, TState, TId>
Expand All @@ -28,7 +28,7 @@ protected CommandService(
IEventStore? store,
AggregateFactoryRegistry? factoryRegistry = null,
StreamNameMap? streamNameMap = null,
TypeMapper? typeMap = null,
ITypeMapper? typeMap = null,
AmendEvent? amendEvent = null
) : this(store, store, factoryRegistry, streamNameMap, typeMap, amendEvent) { }

Expand All @@ -40,7 +40,7 @@ protected CommandService(
readonly HandlersMap<TAggregate, TState, TId> _handlers = new();
readonly AggregateFactoryRegistry _factoryRegistry = factoryRegistry ?? AggregateFactoryRegistry.Instance;
readonly StreamNameMap _streamNameMap = streamNameMap ?? new StreamNameMap();
readonly TypeMapper _typeMap = typeMap ?? TypeMap.Instance;
readonly ITypeMapper _typeMap = typeMap ?? TypeMap.Instance;

/// <summary>
/// Returns the command handler builder for the specified command type.
Expand Down Expand Up @@ -89,7 +89,7 @@ public async Task<Result<TState>> Handle<TCommand>(TCommand command, Cancellatio

var writer = registeredHandler.ResolveWriter(command);
var storeResult = await writer.StoreAggregate<TAggregate, TState>(stream, result, Amend, cancellationToken).NoContext();
var changes = result.Changes.Select(x => new Change(x, _typeMap.GetTypeName(x)));
var changes = result.Changes.Select(x => Change.FromEvent(x, _typeMap));
Log.CommandHandled<TCommand>();

return Result<TState>.FromSuccess(result.State, changes, storeResult.GlobalPosition);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ namespace Eventuous;
using static Diagnostics.ApplicationEventSource;

[Obsolete("Use CommandService<TState>")]
public abstract class FunctionalCommandService<TState>(IEventReader reader, IEventWriter writer, TypeMapper? typeMap = null, AmendEvent? amendEvent = null)
public abstract class FunctionalCommandService<TState>(IEventReader reader, IEventWriter writer, ITypeMapper? typeMap = null, AmendEvent? amendEvent = null)
: CommandService<TState>(reader, writer, typeMap, amendEvent) where TState : State<TState>, new() {
protected FunctionalCommandService(IEventStore store, TypeMapper? typeMap = null, AmendEvent? amendEvent = null)
protected FunctionalCommandService(IEventStore store, ITypeMapper? typeMap = null, AmendEvent? amendEvent = null)
: this(store, store, typeMap, amendEvent) { }

[Obsolete("Use On<TCommand>().InState(ExpectedState.New).GetStream(...).Act(...) instead")]
Expand All @@ -32,22 +32,22 @@ protected void OnAny<TCommand>(Func<TCommand, StreamName> getStreamName, Func<TS
/// </summary>
/// <param name="reader">Event reader or event store</param>
/// <param name="writer">Event writer or event store</param>
/// <param name="typeMap"><seealso cref="TypeMapper"/> instance or null to use the default type mapper</param>
/// <param name="typeMap"><seealso cref="ITypeMapper"/> instance or null to use the default type mapper</param>
/// <param name="amendEvent">Optional function to add extra information to the event before it gets stored</param>
/// <typeparam name="TState">State object type</typeparam>
public abstract class CommandService<TState>(IEventReader reader, IEventWriter writer, TypeMapper? typeMap = null, AmendEvent? amendEvent = null)
public abstract class CommandService<TState>(IEventReader reader, IEventWriter writer, ITypeMapper? typeMap = null, AmendEvent? amendEvent = null)
: ICommandService<TState> where TState : State<TState>, new() {
readonly TypeMapper _typeMap = typeMap ?? TypeMap.Instance;
readonly ITypeMapper _typeMap = typeMap ?? TypeMap.Instance;
readonly HandlersMap<TState> _handlers = new();

/// <summary>
/// Alternative constructor for the functional command service, which uses an <seealso cref="IEventStore"/> instance for both reading and writing.
/// </summary>
/// <param name="store">Event store</param>
/// <param name="typeMap"><seealso cref="TypeMapper"/> instance or null to use the default type mapper</param>
/// <param name="typeMap"><seealso cref="ITypeMapper"/> instance or null to use the default type mapper</param>
/// <param name="amendEvent">Optional function to add extra information to the event before it gets stored</param>
// ReSharper disable once UnusedMember.Global
protected CommandService(IEventStore store, TypeMapper? typeMap = null, AmendEvent? amendEvent = null) : this(store, store, typeMap, amendEvent) { }
protected CommandService(IEventStore store, ITypeMapper? typeMap = null, AmendEvent? amendEvent = null) : this(store, store, typeMap, amendEvent) { }

/// <summary>
/// Returns the command handler builder for the specified command type.
Expand Down Expand Up @@ -96,7 +96,7 @@ public async Task<Result<TState>> Handle<TCommand>(TCommand command, Cancellatio

var storeResult = await resolvedWriter.Store(streamName, loadedState.StreamVersion, newEvents, Amend, cancellationToken)
.NoContext();
var changes = newEvents.Select(x => new Change(x, _typeMap.GetTypeName(x)));
var changes = newEvents.Select(x => Change.FromEvent(x, _typeMap));
Log.CommandHandled<TCommand>();

return Result<TState>.FromSuccess(newState, changes, storeResult.GlobalPosition);
Expand All @@ -105,9 +105,10 @@ public async Task<Result<TState>> Handle<TCommand>(TCommand command, Cancellatio

return Result<TState>.FromError(e, $"Error handling command {typeof(TCommand).Name}");
}

NewStreamEvent Amend(NewStreamEvent streamEvent) {
var evt = registeredHandler.AmendEvent?.Invoke(streamEvent, command) ?? streamEvent;

return amendEvent?.Invoke(evt) ?? evt;
}
}
Expand Down
8 changes: 7 additions & 1 deletion src/Core/src/Eventuous.Application/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
namespace Eventuous;

[StructLayout(LayoutKind.Auto)]
public record struct Change(object Event, string EventType);
public record struct Change(object Event, string EventType) {
internal static Change FromEvent(object evt, ITypeMapper typeMapper) {
var typeName = typeMapper.GetTypeName(evt);

return new(evt, typeName != ITypeMapper.UnknownType ? typeName : evt.GetType().Name);
}
}

/// <summary>
/// Represents the command handling result, could be either success or error
Expand Down
3 changes: 1 addition & 2 deletions src/Core/src/Eventuous.Persistence/AppendEventsResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

namespace Eventuous;

[PublicAPI]
public record AppendEventsResult(ulong GlobalPosition, long NextExpectedVersion) {
public static readonly AppendEventsResult NoOp = new(0, -1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
namespace Eventuous;

[PublicAPI]
public class DefaultEventSerializer(JsonSerializerOptions options, TypeMapper? typeMapper = null) : IEventSerializer {
public class DefaultEventSerializer(JsonSerializerOptions options, ITypeMapper? typeMapper = null) : IEventSerializer {
public static IEventSerializer Instance { get; private set; } = new DefaultEventSerializer(new(JsonSerializerDefaults.Web));

readonly TypeMapper _typeMapper = typeMapper ?? TypeMap.Instance;
readonly ITypeMapper _typeMapper = typeMapper ?? TypeMap.Instance;

public static void SetDefaultSerializer(IEventSerializer serializer) => Instance = serializer;

Expand Down
28 changes: 28 additions & 0 deletions src/Core/src/Eventuous.Shared/TypeMap/ITypeMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (C) Ubiquitous AS.All rights reserved
// Licensed under the Apache License, Version 2.0.

namespace Eventuous;

public interface ITypeMapper {
public const string UnknownType = "unknown";

/// <summary>
/// Try getting a type name for a given type
/// </summary>
/// <param name="type">Type for which the name is requested</param>
/// <param name="typeName">Registered type name or null if the type isn't registered</param>
/// <returns>True if the type is registered, false otherwise</returns>
bool TryGetTypeName(Type type, [NotNullWhen(true)] out string? typeName);

/// <summary>
/// Try getting a registered type for a given name
/// </summary>
/// <param name="typeName">Known type name</param>
/// <param name="type">Registered type for a given name or null if the type name isn't registered</param>
/// <returns>True if the type is registered, false otherwise</returns>
bool TryGetType(string typeName, [NotNullWhen(true)] out Type? type);
}

public interface ITypeMapperExt : ITypeMapper {
IEnumerable<(string TypeName, Type Type)> GetRegisteredTypes();
}
64 changes: 10 additions & 54 deletions src/Core/src/Eventuous.Shared/TypeMap/TypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,64 +25,28 @@ public static class TypeMap {
/// </summary>
/// <param name="assemblies">Zero or more assemblies that contain event classes to scan.
/// If omitted, all the assemblies of the current <seealso cref="AppDomain"/> will be scanned.</param>
public static void RegisterKnownEventTypes(params Assembly[] assemblies) => Instance.RegisterKnownEventTypes(assemblies);
public static void RegisterKnownEventTypes(params Assembly[] assemblies) {
Instance.RegisterKnownEventTypes(assemblies);
}
}

/// <summary>
/// The actual mapper behind static <see cref="TypeMap"/>.
/// </summary>
public class TypeMapper {
public class TypeMapper : ITypeMapperExt {
readonly Dictionary<string, Type> _reverseMap = new();
readonly Dictionary<Type, string> _map = new();

// ReSharper disable once UnusedMember.Global
public IReadOnlyDictionary<Type, string> Map => _map;
public IReadOnlyDictionary<string, Type> ReverseMap => _reverseMap;

[PublicAPI]
public string GetTypeName<T>() {
if (!_map.TryGetValue(typeof(T), out var name)) {
Log.TypeNotMappedToName(typeof(T));

throw new UnregisteredTypeException(typeof(T));
}

return name;
}

public string GetTypeName(object o, bool fail = true) {
if (_map.TryGetValue(o.GetType(), out var name)) return name;

if (!fail) return "unknown";

Log.TypeNotMappedToName(o.GetType());

throw new UnregisteredTypeException(o.GetType());
}

[PublicAPI]
public string GetTypeNameByType(Type type) {
if (!_map.TryGetValue(type, out var name)) {
Log.TypeNotMappedToName(type);

throw new UnregisteredTypeException(type);
}

return name;
}

public Type GetType(string typeName) {
if (!_reverseMap.TryGetValue(typeName, out var type)) {
Log.TypeNameNotMappedToType(typeName);

throw new UnregisteredTypeException(typeName);
}

return type;
}
/// <inheritdoc />>
public bool TryGetTypeName(Type type, [NotNullWhen(true)] out string? typeName) => _map.TryGetValue(type, out typeName);

/// <inheritdoc />>
public bool TryGetType(string typeName, [NotNullWhen(true)] out Type? type) => _reverseMap.TryGetValue(typeName, out type);

public IEnumerable<(string TypeName, Type Type)> GetRegisteredTypes() => _reverseMap.Select(x => (x.Key, x.Value));

/// <summary>
/// Adds a message type to the map.
/// </summary>
Expand Down Expand Up @@ -125,14 +89,12 @@ public void AddType(Type type, string? name = null) {

[MethodImpl(MethodImplOptions.Synchronized)]
public void RemoveType<T>() {
var name = GetTypeName<T>();
var name = this.GetTypeName<T>();

_reverseMap.Remove(name);
_map.Remove(typeof(T));
}

public bool IsTypeRegistered<T>() => _map.ContainsKey(typeof(T));

public void RegisterKnownEventTypes(params Assembly[] assembliesWithEvents) {
var assembliesToScan = assembliesWithEvents.Length == 0 ? GetDefaultAssemblies() : assembliesWithEvents;

Expand Down Expand Up @@ -173,12 +135,6 @@ void RegisterAssemblyEventTypes(Assembly assembly) {
AddType(type, attr.EventType);
}
}

public void EnsureTypesRegistered(IEnumerable<Type> types) {
foreach (var type in types) {
GetTypeNameByType(type);
}
}
}

[AttributeUsage(AttributeTargets.Class)]
Expand Down
59 changes: 59 additions & 0 deletions src/Core/src/Eventuous.Shared/TypeMap/TypeMapperExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (C) Ubiquitous AS.All rights reserved
// Licensed under the Apache License, Version 2.0.

namespace Eventuous;

using static TypeMapEventSource;

public static class TypeMapperExtensions {
/// <summary>
/// Get the type name for a given type
/// </summary>
/// <param name="typeMapper">Type mapper instance</param>
/// <param name="type">Object type for which the name needs to be retrieved</param>
/// <param name="fail">Indicates if exception should be thrown if the type is now registered</param>
/// <returns>Type name from the map or "unknown" if the type isn't registered and <code>fail</code> is set to false</returns>
/// <exception cref="UnregisteredTypeException">Thrown if the type isn't registered and fail is set to true</exception>
public static string GetTypeNameByType(this ITypeMapper typeMapper, Type type, bool fail = true) {
var typeKnown = typeMapper.TryGetTypeName(type, out var name);

if (!typeKnown && fail) {
Log.TypeNotMappedToName(type);

throw new UnregisteredTypeException(type);
}

return name ?? ITypeMapper.UnknownType;
}

public static string GetTypeName(this ITypeMapper typeMapper, object o, bool fail = true) => typeMapper.GetTypeNameByType(o.GetType(), fail);

public static string GetTypeName<T>(this ITypeMapper typeMapper, bool fail = true) => typeMapper.GetTypeNameByType(typeof(T), fail);

public static bool TryGetTypeName<T>(this ITypeMapper typeMapper, [NotNullWhen(true)] out string? typeName) => typeMapper.TryGetTypeName(typeof(T), out typeName);

/// <summary>
/// Get the registered type for a given name
/// </summary>
/// <param name="typeMapper">Type mapper instance</param>
/// <param name="typeName">Type name for which the type needs to be returned</param>
/// <returns>Type that matches the given name</returns>
/// <exception cref="UnregisteredTypeException">Thrown if the type isn't registered and fail is set to true</exception>
public static Type GetType(this ITypeMapper typeMapper, string typeName) {
var typeKnown = typeMapper.TryGetType(typeName, out var type);

if (!typeKnown) {
Log.TypeNameNotMappedToType(typeName);

throw new UnregisteredTypeException(typeName);
}

return type!;
}

public static void EnsureTypesRegistered(this ITypeMapper typeMapper, IEnumerable<Type> types) {
foreach (var type in types) {
typeMapper.GetTypeNameByType(type);
}
}
}
6 changes: 3 additions & 3 deletions src/Core/src/Eventuous.Subscriptions/Handlers/EventHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ namespace Eventuous.Subscriptions;
/// Base class for event handlers, which allows registering typed handlers for different event types
/// </summary>
[PublicAPI]
public abstract class EventHandler(TypeMapper? mapper = null) : BaseEventHandler {
public abstract class EventHandler(ITypeMapper? mapper = null) : BaseEventHandler {
readonly Dictionary<Type, HandleUntypedEvent> _handlersMap = new();

static readonly ValueTask<EventHandlingStatus> Ignored = new(EventHandlingStatus.Ignored);

readonly TypeMapper _typeMapper = mapper ?? TypeMap.Instance;
readonly ITypeMapper _typeMapper = mapper ?? TypeMap.Instance;

/// <summary>
/// Register a handler for a particular event type
Expand All @@ -32,7 +32,7 @@ protected void On<T>(HandleTypedEvent<T> handler) where T : class {
throw new ArgumentException($"Type {typeof(T).Name} already has a handler");
}

if (!_typeMapper.IsTypeRegistered<T>()) {
if (!_typeMapper.TryGetTypeName<T>(out _)) {
SubscriptionsEventSource.Log.MessageTypeNotRegistered<T>();
}

Expand Down
16 changes: 8 additions & 8 deletions src/Core/test/Eventuous.Tests.Application/BookingFuncService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ namespace Eventuous.Tests.Application;
using static Sut.Domain.BookingEvents;

public class BookingFuncService : CommandService<BookingState> {
public BookingFuncService(IEventStore store, TypeMapper? typeMap = null, AmendEvent? amendEvent = null) : base(store, typeMap, amendEvent) {
public BookingFuncService(IEventStore store, ITypeMapper? typeMap = null, AmendEvent? amendEvent = null) : base(store, typeMap, amendEvent) {
On<BookRoom>()
.InState(ExpectedState.New)
.GetStream(cmd => GetStream(cmd.BookingId))
.Act(BookRoom);
.Act(cmd => [new RoomBooked(cmd.RoomId, cmd.CheckIn, cmd.CheckOut, cmd.Price)]);

On<RecordPayment>()
.InState(ExpectedState.Existing)
Expand All @@ -22,14 +22,14 @@ public BookingFuncService(IEventStore store, TypeMapper? typeMap = null, AmendEv
On<ImportBooking>()
.InState(ExpectedState.Any)
.GetStream(cmd => GetStream(cmd.BookingId))
.Act(ImportBooking);
.Act((_, _, cmd) => [new BookingImported(cmd.RoomId, cmd.Price, cmd.CheckIn, cmd.CheckOut)]);

return;

static IEnumerable<object> BookRoom(BookRoom cmd) => [new RoomBooked(cmd.RoomId, cmd.CheckIn, cmd.CheckOut, cmd.Price)];
On<CancelBooking>()
.InState(ExpectedState.Any)
.GetStream(cmd => GetStream(cmd.BookingId))
.Act((_, _, _) => [new BookingCancelled()]);

static IEnumerable<object> ImportBooking(BookingState state, object[] events, ImportBooking cmd)
=> [new BookingImported(cmd.RoomId, cmd.Price, cmd.CheckIn, cmd.CheckOut)];
return;

static IEnumerable<object> RecordPayment(BookingState state, object[] originalEvents, RecordPayment cmd) {
if (state.HasPayment(cmd.PaymentId)) yield break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ protected override ICommandService<BookingState> CreateService(
class ExtendedService : BookingService {
public ExtendedService(
IEventStore store,
TypeMapper typeMap,
ITypeMapper typeMap,
AmendEvent<ImportBooking>? amendEvent = null,
AmendEvent? amendAll = null
) : base(store, typeMapper: typeMap, amendEvent: amendAll) {
Expand Down
Loading

0 comments on commit e5a8461

Please sign in to comment.