Skip to content

Commit

Permalink
Merge pull request #2 from deveel/feature-eventfactory
Browse files Browse the repository at this point in the history
Event Factory Pattern
  • Loading branch information
tsutomi authored Sep 18, 2024
2 parents e5dca36 + 26b5bc3 commit 962fab1
Show file tree
Hide file tree
Showing 17 changed files with 462 additions and 36 deletions.
3 changes: 1 addition & 2 deletions Deveel.Events.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{EDED427E-1408-4971-9FA9-EBFCACCEAC22}"
ProjectSection(SolutionItems) = preProject
test\Directory.Build.props = test\Directory.Build.props
test\Deveel.Events.Model.XUnit\Events\EventSchemaTests.cs = test\Deveel.Events.Model.XUnit\Events\EventSchemaTests.cs
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Events.Schema.XUnit", "test\Deveel.Events.Schema.XUnit\Deveel.Events.Schema.XUnit.csproj", "{9BEFCC78-DBF6-4A3A-B2E3-2A0DD510DEB0}"
Expand All @@ -32,7 +31,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Events.Publisher.Azu
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{2A332BDD-050D-4E45-81BA-896A46C2B42E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeveelEventsBenchmarks", "benchmarks\DeveelEventsBenchmarks\DeveelEventsBenchmarks.csproj", "{1EE44B7D-1BD0-4D03-AD28-4121472D379E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeveelEventsBenchmarks", "benchmarks\DeveelEventsBenchmarks\DeveelEventsBenchmarks.csproj", "{1EE44B7D-1BD0-4D03-AD28-4121472D379E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
39 changes: 38 additions & 1 deletion benchmarks/DeveelEventsBenchmarks/PublishBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace DeveelEventsBenchmarks
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
[RyuJitX64Job, RyuJitX86Job, LegacyJitX64Job, LegacyJitX86Job]
[RyuJitX64Job, RyuJitX86Job]
public class PublishBenchmarks
{
private readonly EventPublisher _publisher;
Expand Down Expand Up @@ -72,12 +72,49 @@ public async Task Publish_EventDataAsParameter()
await _publisher.PublishAsync(@event);
}

[Benchmark]
public async Task Publish_EventFactory()
{
var @event = new PersonDeleted
{
FirstName = "John",
LastName = "Doe"
};

await _publisher.PublishAsync(@event);
}

[Event("person.created", "1.0")]
class PersonCreated
{
public string FirstName { get; set; }

public string LastName { get; set; }
}

class PersonDeleted : IEventFactory
{
public string FirstName { get; set; }

public string LastName { get; set; }

public CloudEvent CreateEvent()
{
return new CloudEvent
{
Type = "person.deleted",
DataSchema = new Uri("http://example.com/schema/1.0"),
Source = new Uri("https://api.svc.deveel.com/test-service"),
Time = DateTime.UtcNow,
Id = Guid.NewGuid().ToString("N"),
DataContentType = "application/cloudevents+json",
Data = JsonSerializer.Serialize(new
{
FirstName,
LastName
}),
};
}
}
}
}
Binary file added deveel-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 31 additions & 2 deletions src/Deveel.Events.Model/Events/EventAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
using System;

namespace Deveel.Events {
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
/// <summary>
/// An attribute that is used to describe an event type and its
/// versioned schema.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class EventAttribute : Attribute {
public EventAttribute(string eventType, string dataSchemaOrVersion) {
/// <summary>
/// Constructs an attribute that describes an event type
/// and its version or the URL to the schema.
/// </summary>
/// <param name="eventType">
/// The type of the event that is being described.
/// </param>
/// <param name="dataSchemaOrVersion">
/// The URL to the schema or the version of the event.
/// </param>
/// <exception cref="ArgumentNullException">
/// Thrown when the event type or the schema/version is <c>null</c>.
/// </exception>
public EventAttribute(string eventType, string dataSchemaOrVersion) {
ArgumentNullException.ThrowIfNull(eventType, nameof(eventType));
ArgumentNullException.ThrowIfNull(dataSchemaOrVersion, nameof(dataSchemaOrVersion));

Expand All @@ -18,12 +35,24 @@ public EventAttribute(string eventType, string dataSchemaOrVersion) {
}
}

/// <summary>
/// The type of the event that is being described.
/// </summary>
public string EventType { get; }

/// <summary>
/// The URL to the schema of the event.
/// </summary>
public Uri? DataSchema { get; }

/// <summary>
/// The version of the event.
/// </summary>
public string? DataVersion { get; set; }

/// <summary>
/// A description of the event type.
/// </summary>
public string? Description { get; set; }
}
}
28 changes: 26 additions & 2 deletions src/Deveel.Events.Model/Events/EventAttributesAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
namespace Deveel.Events {
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
/// <summary>
/// An attribute that can be used to describe additional attributes
/// (such as metadata) that can be attached to an event
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class EventAttributesAttribute : Attribute {
public EventAttributesAttribute(string attributeName, object? value) {
/// <summary>
/// Constructs an attribute with the given name and value.
/// </summary>
/// <param name="attributeName">
/// The name of the attribute that is used to uniquely identify it
/// within the event.
/// </param>
/// <param name="value">
/// The constant value of the attribute that is being set.
/// </param>
/// <exception cref="ArgumentNullException">
/// Thrown when the attribute name is <c>null</c>.
/// </exception>
public EventAttributesAttribute(string attributeName, object? value) {
ArgumentNullException.ThrowIfNull(attributeName, nameof(attributeName));

AttributeName = attributeName;
Value = value;
}

/// <summary>
/// Gets the name of the attribute that is used to uniquely
/// identify it within the event.
/// </summary>
public string AttributeName { get; }

/// <summary>
/// Gets the constant value of the attribute that is being set.
/// </summary>
public object? Value { get; }
}
}
36 changes: 32 additions & 4 deletions src/Deveel.Events.Model/Events/EventPropertyAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
namespace Deveel.Events {
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false)]
/// <summary>
/// An attribute that is used to mark a property of a
/// type as a property of the payload of the event.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false)]
public sealed class EventPropertyAttribute : Attribute {
public EventPropertyAttribute(string? name, string? schemaOrVersion = null) {
/// <summary>
/// Constructs an event property attribute with the given name
/// and the optional schema or version of the event this property
/// belongs to.
/// </summary>
/// <param name="name">
/// The name of the property that is used to identify it
/// within the event.
/// </param>
/// <param name="schemaOrVersion"></param>
/// <exception cref="ArgumentException"></exception>
public EventPropertyAttribute(string? name, string? schemaOrVersion = null) {
if (!String.IsNullOrWhiteSpace(schemaOrVersion)) {
if (System.Version.TryParse(schemaOrVersion, out _))
{
Expand All @@ -19,12 +34,25 @@ public EventPropertyAttribute(string? name, string? schemaOrVersion = null) {
Version = schemaOrVersion;
}

public string? Name { get; }
/// <summary>
/// The name of the property that is used to identify it
/// within the event.
/// </summary>
public string? Name { get; }

public string? Description { get; set; }
/// <summary>
/// A description of the property for documentation purposes.
/// </summary>
public string? Description { get; set; }

/// <summary>
/// The version of the event this property belongs to.
/// </summary>
public string? Version { get; set; }

/// <summary>
/// The schema of the data that is part of the property.
/// </summary>
public Uri? Schema { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
namespace Deveel.Events {
static partial class LoggerExtensions {
[LoggerMessage(30001, LogLevel.Debug, "Event of type {EventType} to be published")]
public static partial void TracePublishingEvent(this ILogger logger, string eventType);
public static partial void TracePublishingEvent(this ILogger logger, string? eventType);

[LoggerMessage(30002, LogLevel.Error, "Error while publishing an event of type '{EventType}'")]
public static partial void LogErrorPublishingEvent(this ILogger logger, Exception ex, string eventType);
public static partial void LogErrorPublishingEvent(this ILogger logger, Exception ex, string? eventType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ public async Task PublishAsync(CloudEvent @event, CancellationToken cancellation
ThrowIfDisposed();
cancellationToken.ThrowIfCancellationRequested();

logger.TracePublishingEvent(@event.Type);
ArgumentNullException.ThrowIfNull(@event, nameof(@event));

logger.TracePublishingEvent(@event.Type);

try {
await sender!.SendMessageAsync(messageCreator.CreateMessage(@event), cancellationToken);
Expand Down
27 changes: 26 additions & 1 deletion src/Deveel.Events.Publisher/Events/EventPublisher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

using System.ComponentModel.DataAnnotations;

namespace Deveel.Events {
public class EventPublisher {

Check warning on line 10 in src/Deveel.Events.Publisher/Events/EventPublisher.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 7.0.x)

Missing XML comment for publicly visible type or member 'EventPublisher'

Check warning on line 10 in src/Deveel.Events.Publisher/Events/EventPublisher.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest, 7.0.x)

Missing XML comment for publicly visible type or member 'EventPublisher'

Check warning on line 10 in src/Deveel.Events.Publisher/Events/EventPublisher.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest, 8.0.x)

Missing XML comment for publicly visible type or member 'EventPublisher'

Check warning on line 10 in src/Deveel.Events.Publisher/Events/EventPublisher.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest, 6.0.x)

Missing XML comment for publicly visible type or member 'EventPublisher'
private readonly IEnumerable<IEventPublishChannel> _channels;
Expand Down Expand Up @@ -139,5 +141,28 @@ protected virtual CloudEvent CreateEventFromData(Type dataType, object? data)

public Task PublishAsync<TData>(TData data, CancellationToken cancellationToken = default)
=> PublishAsync(typeof(TData), data, cancellationToken);
}

public Task PublishEventAsync<T>(T factory, CancellationToken cancellationToken = default)
where T : IEventFactory
{
ArgumentNullException.ThrowIfNull(factory, nameof(factory));

CloudEvent @event;

try
{
@event = factory.CreateEvent();
} catch (Exception ex)
{
_logger.LogEventFactoryError(ex, factory.GetType());

if (PublisherOptions.ThrowOnErrors)
throw new EventPublishException($"An error occurred while creating an event using the factory {factory.GetType().FullName}", ex);

return Task.CompletedTask;
}

return PublishEventAsync(@event, cancellationToken);
}
}
}
9 changes: 9 additions & 0 deletions src/Deveel.Events.Publisher/Events/IEventFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using CloudNative.CloudEvents;

namespace Deveel.Events
{
public interface IEventFactory
{
CloudEvent CreateEvent();
}
}
5 changes: 4 additions & 1 deletion src/Deveel.Events.Publisher/Events/LoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ static partial class LoggerExtensions {
[LoggerMessage(-30001, LogLevel.Error, "Could not create the event of type '{EventType}'")]
public static partial void LogEventCreateError(this ILogger logger, Exception ex, Type eventType);

[LoggerMessage(-30002, LogLevel.Error, "Could not publish the event of type '{EventType}' through the channel of type '{ChannelType}'")]
[LoggerMessage(-30002, LogLevel.Error, "Could not create the event from the factory of type '{FactoryType}'")]
public static partial void LogEventFactoryError(this ILogger logger, Exception ex, Type factoryType);

[LoggerMessage(-30002, LogLevel.Error, "Could not publish the event of type '{EventType}' through the channel of type '{ChannelType}'")]

Check warning on line 11 in src/Deveel.Events.Publisher/Events/LoggerExtensions.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 6.0.x)

Multiple logging methods are using event id -30002 in class LoggerExtensions

Check warning on line 11 in src/Deveel.Events.Publisher/Events/LoggerExtensions.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 8.0.x)

Multiple logging methods are using event id -30002 in class LoggerExtensions
public static partial void LogEventPublishError(this ILogger logger, Exception ex, string eventType, Type channelType);

[LoggerMessage(400012, LogLevel.Debug, "Publishing an event of type '{EventType}' through the channel of type '{ChannelType}'")]
Expand Down
24 changes: 22 additions & 2 deletions src/Deveel.Events.Schema/Events/EnumMemberConstraint.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
namespace Deveel.Events {
public class EnumMemberConstraint<TValue> : IEventPropertyConstraint {
public EnumMemberConstraint(IReadOnlyList<TValue> allowedValues) {
/// <summary>
/// A constraint that is used to restrict the values of a
/// property to a set of allowed ones.
/// </summary>
/// <typeparam name="TValue">
/// The type of values that allowed by the constraint.
/// </typeparam>
public class EnumMemberConstraint<TValue> : IEventPropertyConstraint {
/// <summary>
/// Constructs a constraint that allows only the values
/// enumerated in the given list.
/// </summary>
/// <param name="allowedValues">
/// The list of values that are allowed by the constraint.
/// </param>
/// <exception cref="ArgumentNullException">
/// Throws when the list of allowed values is <c>null</c>.
/// </exception>
public EnumMemberConstraint(IReadOnlyList<TValue> allowedValues) {
AllowedValues = allowedValues ?? throw new ArgumentNullException(nameof(allowedValues));
}

/// <summary>
/// The list of values that are allowed by the constraint.
/// </summary>
public IReadOnlyList<TValue> AllowedValues { get; }

bool IEventPropertyConstraint.IsValid(object? value) {
Expand Down
Loading

0 comments on commit 962fab1

Please sign in to comment.