diff --git a/Source/Analyzers/AggregateAnalyzer.cs b/Source/Analyzers/AggregateAnalyzer.cs
index 7d086eee..b2f5275f 100644
--- a/Source/Analyzers/AggregateAnalyzer.cs
+++ b/Source/Analyzers/AggregateAnalyzer.cs
@@ -28,7 +28,8 @@ public class AggregateAnalyzer : DiagnosticAnalyzer
DescriptorRules.Aggregate.MutationShouldBePrivate,
DescriptorRules.Aggregate.MutationHasIncorrectNumberOfParameters,
DescriptorRules.Aggregate.MutationsCannotProduceEvents,
- DescriptorRules.Events.MissingAttribute
+ DescriptorRules.Events.MissingAttribute,
+ DescriptorRules.Aggregate.PublicMethodsCannotMutateAggregateState
);
///
@@ -53,7 +54,8 @@ static void AnalyzeAggregates(SyntaxNodeAnalysisContext context)
var handledEvents = CheckOnMethods(context, aggregateSymbol);
CheckApplyInvocations(context, aggregateSyntax, handledEvents);
- CheckApplyInvocationsInOnMethods(context, aggregateSyntax, aggregateSymbol);
+ CheckApplyInvocationsInOnMethods(context, aggregateSymbol);
+ CheckMutationsInPublicMethods(context, aggregateSymbol);
}
@@ -144,10 +146,8 @@ static void CheckApplyInvocations(SyntaxNodeAnalysisContext context, ClassDeclar
}
}
- static void CheckApplyInvocationsInOnMethods(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax aggregateClassSyntax, INamedTypeSymbol aggregateType)
+ static void CheckApplyInvocationsInOnMethods(SyntaxNodeAnalysisContext context, INamedTypeSymbol aggregateType)
{
- var semanticModel = context.SemanticModel;
-
var onMethods = aggregateType
.GetMembers()
.Where(member => member.Name.Equals("On"))
@@ -194,4 +194,62 @@ static void CheckApplyInvocationsInOnMethods(SyntaxNodeAnalysisContext context,
}
}
}
-}
+
+ static void CheckMutationsInPublicMethods(SyntaxNodeAnalysisContext context, INamedTypeSymbol aggregateType)
+ {
+ var publicMethods = aggregateType
+ .GetMembers()
+ .Where(member => !member.Name.Equals("On"))
+ .OfType()
+ .Where(method => method.DeclaredAccessibility.HasFlag(Accessibility.Public))
+ .ToArray();
+ if (publicMethods.Length == 0)
+ {
+ return;
+ }
+ var walker = new MutationWalker(context, aggregateType);
+
+ foreach (var onMethod in publicMethods)
+ {
+ if (onMethod.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is not MethodDeclarationSyntax syntax)
+ {
+ continue;
+ }
+ walker.Visit(syntax);
+ }
+ }
+
+ class MutationWalker : CSharpSyntaxWalker
+ {
+ readonly SyntaxNodeAnalysisContext _context;
+ readonly INamedTypeSymbol _aggregateType;
+
+ public MutationWalker(SyntaxNodeAnalysisContext context, INamedTypeSymbol aggregateType)
+ {
+ _context = context;
+ _aggregateType = aggregateType;
+ }
+
+ public override void VisitAssignmentExpression(AssignmentExpressionSyntax node)
+ {
+ var leftExpression = node.Left;
+
+ if (leftExpression is IdentifierNameSyntax || leftExpression is MemberAccessExpressionSyntax)
+ {
+ var symbolInfo = _context.SemanticModel.GetSymbolInfo(leftExpression);
+ if (symbolInfo.Symbol is IFieldSymbol || symbolInfo.Symbol is IPropertySymbol)
+ {
+ var containingType = symbolInfo.Symbol.ContainingType;
+ if (containingType != null && SymbolEqualityComparer.Default.Equals(_aggregateType, containingType))
+ {
+ var diagnostic = Diagnostic.Create(DescriptorRules.Aggregate.PublicMethodsCannotMutateAggregateState, leftExpression.GetLocation());
+ _context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+
+ base.VisitAssignmentExpression(node);
+ }
+
+ // You can also add other types of mutations like increments, decrements, method calls etc.
+ }}
diff --git a/Source/Analyzers/AnalysisExtensions.cs b/Source/Analyzers/AnalysisExtensions.cs
index 179744df..a62d8e59 100644
--- a/Source/Analyzers/AnalysisExtensions.cs
+++ b/Source/Analyzers/AnalysisExtensions.cs
@@ -125,4 +125,5 @@ static bool MatchesName(this AttributeArgumentSyntax attributeArgument, string p
{
return attributeArgument.NameColon?.Name.Identifier.Text == parameterName;
}
+
}
diff --git a/Source/Analyzers/CodeFixes/EventHandlerEventContextCodeFixProvider.cs b/Source/Analyzers/CodeFixes/EventHandlerEventContextCodeFixProvider.cs
new file mode 100644
index 00000000..9a428e8f
--- /dev/null
+++ b/Source/Analyzers/CodeFixes/EventHandlerEventContextCodeFixProvider.cs
@@ -0,0 +1,119 @@
+// Copyright (c) Dolittle. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Dolittle.SDK.Analyzers.CodeFixes;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AttributeMissingCodeFixProvider)), Shared]
+public class EventHandlerEventContextCodeFixProvider : CodeFixProvider
+{
+ public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(
+ DiagnosticIds.EventHandlerMissingEventContext
+ );
+
+ public override Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var document = context.Document;
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ switch (diagnostic.Id)
+ {
+ case DiagnosticIds.EventHandlerMissingEventContext:
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ "Add EventContext parameter",
+ ct => AddEventContextParameter(context, diagnostic, document, ct),
+ nameof(EventHandlerEventContextCodeFixProvider) + ".AddEventContext"),
+ diagnostic);
+ break;
+ }
+ }
+
+
+ return Task.CompletedTask;
+ }
+
+ async Task AddEventContextParameter(CodeFixContext context, Diagnostic diagnostic, Document document, CancellationToken ct)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null) return document;
+
+ // Find the method declaration identified by the diagnostic.
+ var methodDeclaration = GetMethodDeclaration(root, diagnostic);
+ if (methodDeclaration is null)
+ {
+ return document;
+ }
+
+
+ var updatedRoot = root.ReplaceNode(methodDeclaration, WithEventContextParameter(methodDeclaration));
+ var newRoot = EnsureNamespaceImported((CompilationUnitSyntax)updatedRoot, "Dolittle.SDK.Events");
+ return document.WithSyntaxRoot(newRoot);
+ }
+
+ ///
+ /// Adds EventContext parameter to the method declaration
+ ///
+ ///
+ ///
+ MethodDeclarationSyntax WithEventContextParameter(MethodDeclarationSyntax methodDeclaration)
+ {
+ var existingParameters = methodDeclaration.ParameterList.Parameters;
+ // Get the first parameter that is not the EventContext parameter
+ var eventParameter = existingParameters.FirstOrDefault(parameter => parameter.Type?.ToString() != "EventContext");
+ if (eventParameter is null)
+ {
+ return methodDeclaration;
+ }
+
+
+ var eventContextParameter = SyntaxFactory.Parameter(SyntaxFactory.Identifier("ctx")).WithType(SyntaxFactory.ParseTypeName("EventContext"));
+
+ var originalParameterList = methodDeclaration.ParameterList;
+ var newParameterList = SyntaxFactory.ParameterList(
+ SyntaxFactory.SeparatedList(
+ new[]
+ {
+ eventParameter,
+ eventContextParameter
+ }
+ )
+ ).WithLeadingTrivia(originalParameterList.GetLeadingTrivia())
+ .WithTrailingTrivia(originalParameterList.GetTrailingTrivia());
+ return methodDeclaration.WithParameterList(newParameterList);
+ }
+
+ MethodDeclarationSyntax? GetMethodDeclaration(SyntaxNode root, Diagnostic diagnostic)
+ {
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+ var methodDeclaration = root.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First();
+ return methodDeclaration;
+ }
+
+ public static CompilationUnitSyntax EnsureNamespaceImported(CompilationUnitSyntax root, string namespaceToInclude)
+ {
+ var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(namespaceToInclude));
+ var existingUsings = root.Usings;
+
+ if (existingUsings.Any(u => u.Name?.ToFullString() == namespaceToInclude))
+ {
+ // Namespace is already imported.
+ return root;
+ }
+ var lineEndingTrivia = root.DescendantTrivia().First(_ => _.IsKind(SyntaxKind.EndOfLineTrivia));
+ usingDirective = usingDirective.WithTrailingTrivia(lineEndingTrivia);
+
+ return root.WithUsings(existingUsings.Add(usingDirective));
+ }
+}
diff --git a/Source/Analyzers/DescriptorRules.cs b/Source/Analyzers/DescriptorRules.cs
index 6c9b880e..3989fcd8 100644
--- a/Source/Analyzers/DescriptorRules.cs
+++ b/Source/Analyzers/DescriptorRules.cs
@@ -7,6 +7,26 @@ namespace Dolittle.SDK.Analyzers;
static class DescriptorRules
{
+ internal static readonly DiagnosticDescriptor InvalidTimestamp =
+ new(
+ DiagnosticIds.InvalidTimestampParameter,
+ title: "Invalid DateTimeOffset format",
+ messageFormat: "Value '{0}' should be a valid DateTimeOffset",
+ DiagnosticCategories.Sdk,
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "The value should be a valid DateTimeOffset.");
+
+ internal static readonly DiagnosticDescriptor InvalidStartStopTimestamp =
+ new(
+ DiagnosticIds.InvalidStartStopTime,
+ title: "Start is not before stop",
+ messageFormat: "'{0}' should be before '{1}'",
+ DiagnosticCategories.Sdk,
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "Start timestamp should be before stop timestamp.");
+
internal static readonly DiagnosticDescriptor InvalidIdentity =
new(
DiagnosticIds.AttributeInvalidIdentityRuleId,
@@ -26,7 +46,17 @@ static class DescriptorRules
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Assign a unique identity in the attribute");
-
+
+ internal static readonly DiagnosticDescriptor InvalidAccessibility =
+ new(
+ DiagnosticIds.InvalidAccessibility,
+ title: "Invalid accessibility level",
+ messageFormat: "{0} needs to be '{1}'",
+ DiagnosticCategories.Sdk,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "Change the accessibility level to '{1}'.");
+
internal static class Events
{
internal static readonly DiagnosticDescriptor MissingAttribute =
@@ -38,6 +68,16 @@ internal static class Events
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Mark the event with an EventTypeAttribute and assign an identifier to it");
+
+ internal static readonly DiagnosticDescriptor MissingEventContext =
+ new(
+ DiagnosticIds.EventHandlerMissingEventContext,
+ title: "Handle method does not take EventContext as the second parameter",
+ messageFormat: "{0} is missing EventContext argument",
+ DiagnosticCategories.Sdk,
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "Add the EventContext as the second parameter to the Handle method");
}
internal static class Aggregate
@@ -90,5 +130,14 @@ internal static class Aggregate
DiagnosticSeverity.Error,
isEnabledByDefault: true
);
+
+ internal static readonly DiagnosticDescriptor PublicMethodsCannotMutateAggregateState = new(
+ DiagnosticIds.PublicMethodsCannotMutateAggregateState,
+ "Aggregates should only be mutated with events",
+ "Public methods can not mutate the state of an aggregate. All mutations needs to be done via events.",
+ DiagnosticCategories.Sdk,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
}
}
diff --git a/Source/Analyzers/DiagnosticIds.cs b/Source/Analyzers/DiagnosticIds.cs
index a90d1962..8b919185 100644
--- a/Source/Analyzers/DiagnosticIds.cs
+++ b/Source/Analyzers/DiagnosticIds.cs
@@ -20,6 +20,20 @@ public static class DiagnosticIds
///
public const string IdentityIsNotUniqueRuleId = "SDK0003";
+ ///
+ /// Invalid timestamp.
+ ///
+ public const string InvalidTimestampParameter = "SDK0004";
+
+ ///
+ /// Invalid timestamp.
+ ///
+ public const string InvalidStartStopTime = "SDK0005";
+
+ public const string InvalidAccessibility = "SDK0006";
+
+ public const string EventHandlerMissingEventContext = "SDK0007";
+
///
/// Aggregate missing the required Attribute.
///
@@ -44,4 +58,12 @@ public static class DiagnosticIds
/// Apply can not be used in an On-method.
///
public const string AggregateMutationsCannotProduceEvents = "AGG0005";
+
+ ///
+ /// Public methods can not mutate the state of an aggregate.
+ /// All mutations need to be done in On-methods.
+ ///
+ public const string PublicMethodsCannotMutateAggregateState = "AGG0006";
+
+
}
diff --git a/Source/Analyzers/DolittleTypes.cs b/Source/Analyzers/DolittleTypes.cs
index 1ada642f..5417d5e6 100644
--- a/Source/Analyzers/DolittleTypes.cs
+++ b/Source/Analyzers/DolittleTypes.cs
@@ -12,5 +12,7 @@ static class DolittleTypes
public const string ICommitEventsInterface = "Dolittle.SDK.Events.Store.ICommitEvents";
+ public const string EventContext = "Dolittle.SDK.Events.EventContext";
+
}
diff --git a/Source/Analyzers/EventHandlerAnalyzer.cs b/Source/Analyzers/EventHandlerAnalyzer.cs
new file mode 100644
index 00000000..4250beac
--- /dev/null
+++ b/Source/Analyzers/EventHandlerAnalyzer.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Dolittle.SDK.Analyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class EventHandlerAnalyzer : DiagnosticAnalyzer
+{
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(
+ DescriptorRules.InvalidTimestamp,
+ DescriptorRules.InvalidStartStopTimestamp,
+ DescriptorRules.InvalidAccessibility,
+ DescriptorRules.Events.MissingAttribute,
+ DescriptorRules.Events.MissingEventContext
+ );
+
+ const int StartFromTimestampOffset = 6;
+ const int StopAtTimestampOffset = 7;
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.RegisterSyntaxNodeAction(AnalyzeEventHandler, ImmutableArray.Create(SyntaxKind.ClassDeclaration));
+ }
+
+ void AnalyzeEventHandler(SyntaxNodeAnalysisContext context)
+ {
+ var classSyntax = (ClassDeclarationSyntax)context.Node;
+ var classSymbol = context.SemanticModel.GetDeclaredSymbol(classSyntax);
+ if (classSymbol is null || !classSymbol.HasAttribute(DolittleTypes.EventHandlerAttribute))
+ {
+ return;
+ }
+
+ AnalyzeEventHandlerAttribute(context, classSymbol);
+ AnalyzeHandleMethods(context, classSymbol);
+ }
+
+ void AnalyzeHandleMethods(SyntaxNodeAnalysisContext context, INamedTypeSymbol classSymbol)
+ {
+ var handleMethods = classSymbol.GetMembers()
+ .OfType()
+ .Where(method => method.Name == "Handle");
+
+ foreach (var handleMethod in handleMethods)
+ {
+ AnalyzeHandleMethod(context, handleMethod);
+ }
+ }
+
+ void AnalyzeHandleMethod(SyntaxNodeAnalysisContext context, IMethodSymbol handleMethod)
+ {
+ // Check that it is public
+ if (!handleMethod.DeclaredAccessibility.HasFlag(Accessibility.Public))
+ {
+ var diagnostic = Diagnostic.Create(DescriptorRules.InvalidAccessibility, handleMethod.Locations[0], handleMethod.Name, Accessibility.Public);
+ context.ReportDiagnostic(diagnostic);
+ }
+
+ // Get the first parameter and get the type
+ var parameter = handleMethod.Parameters.FirstOrDefault();
+ if (parameter is null)
+ {
+ return;
+ }
+ // Check if the type has the EventType attribute
+ if (!parameter.Type.HasEventTypeAttribute())
+ {
+ var diagnostic = Diagnostic.Create(DescriptorRules.Events.MissingAttribute, parameter.Locations[0],
+ parameter.Type.ToTargetClassAndAttributeProps(DolittleTypes.EventTypeAttribute), parameter.Type.Name);
+ context.ReportDiagnostic(diagnostic);
+ }
+
+ // Check that the method takes an EventContext as the second parameter
+ var secondParameter = handleMethod.Parameters.Skip(1).FirstOrDefault();
+ if(secondParameter is null || secondParameter.Type.ToString() != DolittleTypes.EventContext)
+ {
+ var diagnostic = Diagnostic.Create(DescriptorRules.Events.MissingEventContext, handleMethod.Locations[0], handleMethod.Name);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ static void AnalyzeEventHandlerAttribute(SyntaxNodeAnalysisContext context, INamedTypeSymbol classSymbol)
+ {
+ var eventHandlerAttribute = classSymbol.GetAttributes()
+ .Single(attribute => attribute.AttributeClass?.ToDisplayString() == DolittleTypes.EventHandlerAttribute);
+ if (eventHandlerAttribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken) is not AttributeSyntax attributeSyntaxNode)
+ {
+ return;
+ }
+
+ AnalyzeEventHandlerStartFromStopAt(context, eventHandlerAttribute, attributeSyntaxNode);
+ }
+
+
+ static void AnalyzeEventHandlerStartFromStopAt(SyntaxNodeAnalysisContext context, AttributeData eventHandlerAttribute, AttributeSyntax syntaxNode)
+ {
+ var startFrom = GetAttributeArgument(eventHandlerAttribute, syntaxNode, StartFromTimestampOffset, "startFromTimestamp");
+ if (startFrom?.value is not null && !DateTimeOffset.TryParse(startFrom.Value.value.ToString(), out var parsedStartFrom))
+ {
+ var diagnostic = Diagnostic.Create(DescriptorRules.InvalidTimestamp, startFrom.Value.location, "startFromTimestamp");
+ context.ReportDiagnostic(diagnostic);
+ }
+
+ var stopAt = GetAttributeArgument(eventHandlerAttribute, syntaxNode, StopAtTimestampOffset, "stopAtTimestamp");
+ if (stopAt?.value is not null && !DateTimeOffset.TryParse(stopAt.Value.value.ToString(), out var parsedStopAt))
+ {
+ var diagnostic = Diagnostic.Create(DescriptorRules.InvalidTimestamp, stopAt.Value.location, "stopAtTimestamp");
+ context.ReportDiagnostic(diagnostic);
+ }
+
+ if(parsedStartFrom > DateTimeOffset.MinValue && parsedStopAt > DateTimeOffset.MinValue && parsedStartFrom >= parsedStopAt)
+ {
+ var diagnostic = Diagnostic.Create(DescriptorRules.InvalidStartStopTimestamp, syntaxNode.GetLocation(), "startFromTimestamp", "stopAtTimestamp");
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ public static (object? value, Location location)? GetAttributeArgument(AttributeData eventHandlerAttribute, AttributeSyntax syntaxNode, int offset,
+ string argumentName)
+ {
+ if (syntaxNode.ArgumentList is null)
+ {
+ return null;
+ }
+
+ // Handle positional arguments
+ if (offset > eventHandlerAttribute.ConstructorArguments.Length)
+ {
+ return null;
+ }
+
+ var argument = eventHandlerAttribute.ConstructorArguments[offset];
+
+ foreach (var argumentSyntax in syntaxNode.ArgumentList.Arguments)
+ {
+ if (argumentSyntax.NameColon?.Name?.Identifier.Text.Equals(argumentName, StringComparison.OrdinalIgnoreCase) == true)
+ {
+ return (argument.Value, argumentSyntax.GetLocation());
+ }
+ }
+
+ if (syntaxNode.ArgumentList.Arguments.Count > offset)
+ {
+ var positionalSyntax = syntaxNode.ArgumentList.Arguments[offset];
+ return (argument.Value, positionalSyntax.GetLocation());
+ }
+
+ return (argument.Value, syntaxNode.GetLocation());
+ }
+}
diff --git a/Source/Events.Handling/Builder/EventHandlerBuilder.cs b/Source/Events.Handling/Builder/EventHandlerBuilder.cs
index 4bc607c3..2e9e8975 100644
--- a/Source/Events.Handling/Builder/EventHandlerBuilder.cs
+++ b/Source/Events.Handling/Builder/EventHandlerBuilder.cs
@@ -21,6 +21,9 @@ public class EventHandlerBuilder : IEventHandlerBuilder, ICanTryBuildEventHandle
bool _partitioned = true;
int _concurrency = 1;
ScopeId _scopeId = ScopeId.Default;
+ ProcessFrom _resetTo;
+ DateTimeOffset? _startFrom;
+ DateTimeOffset? _stopAt;
EventHandlerAlias? _alias;
///
@@ -39,7 +42,7 @@ public EventHandlerBuilder(EventHandlerId eventHandlerId, IModelBuilder modelBui
///
/// Gets the .
///
- public EventHandlerModelId ModelId => new(_eventHandlerId, _partitioned, _scopeId, alias: _alias?.Value, concurrency: _concurrency);
+ public EventHandlerModelId ModelId => new(_eventHandlerId, _partitioned, _scopeId, alias: _alias?.Value, concurrency: _concurrency,_resetTo, _startFrom, _stopAt);
///
public IEventHandlerMethodsBuilder Partitioned()
@@ -87,6 +90,34 @@ public IEventHandlerBuilder WithAlias(EventHandlerAlias alias)
return this;
}
+ ///
+ public IEventHandlerBuilder StartFrom(ProcessFrom processFrom)
+ {
+ Unbind();
+ _resetTo = processFrom;
+ Bind();
+ return this;
+ }
+
+ ///
+ public IEventHandlerBuilder StartFrom(DateTimeOffset processFrom)
+ {
+ Unbind();
+ _startFrom = processFrom;
+ Bind();
+ return this;
+ }
+
+ ///
+ public IEventHandlerBuilder StopAt(DateTimeOffset stopAt)
+ {
+ Unbind();
+ _stopAt = stopAt;
+ Bind();
+ return this;
+ }
+
+
///
public bool TryBuild(EventHandlerModelId identifier, IEventTypes eventTypes, IClientBuildResults buildResults, out IEventHandler eventHandler)
{
diff --git a/Source/Events.Handling/Builder/IEventHandlerBuilder.cs b/Source/Events.Handling/Builder/IEventHandlerBuilder.cs
index bab07aee..5077c9de 100644
--- a/Source/Events.Handling/Builder/IEventHandlerBuilder.cs
+++ b/Source/Events.Handling/Builder/IEventHandlerBuilder.cs
@@ -1,6 +1,7 @@
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System;
using Dolittle.SDK.Events.Handling.Builder.Methods;
namespace Dolittle.SDK.Events.Handling.Builder;
@@ -41,4 +42,27 @@ public interface IEventHandlerBuilder
/// The .
/// The builder for continuation.
IEventHandlerBuilder WithAlias(EventHandlerAlias alias);
+
+ ///
+ /// Set where in the stream the event handler starts if it has no state
+ ///
+ ///
+ ///
+ IEventHandlerBuilder StartFrom(ProcessFrom processFrom);
+
+ ///
+ /// Set when in the stream the event handler starts if it has no state.
+ /// Overrides if set
+ ///
+ /// Timestamp to start processing from
+ ///
+ IEventHandlerBuilder StartFrom(DateTimeOffset processFrom);
+
+ ///
+ /// Optional
+ /// If the event handler should not process newer events than the given timestamp, this can be set.
+ ///
+ ///
+ ///
+ IEventHandlerBuilder StopAt(DateTimeOffset stopAt);
}
diff --git a/Source/Events.Handling/EventHandler.cs b/Source/Events.Handling/EventHandler.cs
index 094d3449..cffa01fa 100644
--- a/Source/Events.Handling/EventHandler.cs
+++ b/Source/Events.Handling/EventHandler.cs
@@ -8,7 +8,6 @@
using System.Threading;
using System.Threading.Tasks;
using Diagnostics;
-using Dolittle.Runtime.Events.Processing.Contracts;
using Dolittle.SDK.Diagnostics.OpenTelemetry;
using Dolittle.SDK.Events.Handling.Builder.Methods;
using Dolittle.SDK.Execution;
diff --git a/Source/Events.Handling/EventHandlerAttribute.cs b/Source/Events.Handling/EventHandlerAttribute.cs
index 3c55668f..2aed8fa5 100644
--- a/Source/Events.Handling/EventHandlerAttribute.cs
+++ b/Source/Events.Handling/EventHandlerAttribute.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
+using System.Globalization;
using Dolittle.SDK.Common.Model;
namespace Dolittle.SDK.Events.Handling;
@@ -23,35 +24,39 @@ public class EventHandlerAttribute : Attribute, IDecoratedTypeDecoratorThe scope that the event handler handles events in.
/// The alias for the event handler.
/// How many events can be processed simultaneously
- // /// Where to start processing if the event handler does not have state. Defaults to the first event in the log.
- // /// Determines a specific event timestamp to start at if set. Overrides resetTo when used.
- // /// Determines a specific event timestamp to stop processing at if set.
+ /// Where in the event log to start processing if the event handler has not been used before. Defaults to the first event.
+ /// Determines a specific event timestamp to start at if set. Overrides startFrom when used. Format is ISO 8601
+ /// Determines a specific event timestamp to stop processing at if set. Format is ISO 8601
public EventHandlerAttribute(
string eventHandlerId,
bool partitioned = true,
string? inScope = null,
string? alias = null,
- int concurrency = 1
- // ,
- // ProcessFrom resetTo = ProcessFrom.First,
- // string? startFrom = null,
- // string? stopAt = null
- )
+ int concurrency = 1,
+ ProcessFrom startFrom = ProcessFrom.Earliest,
+ string? startFromTimestamp = null,
+ string? stopAtTimestamp = null
+ )
{
_eventHandlerId = eventHandlerId;
_alias = alias;
Concurrency = concurrency;
- // ResetTo = resetTo;
- // StartFrom = startFrom;
- // StopAt = stopAt;
+ StartFrom = startFrom;
+ StartFromTimestamp = string.IsNullOrWhiteSpace(startFromTimestamp) ? null : DateTimeOffset.Parse(startFromTimestamp, CultureInfo.InvariantCulture);
+ StopAtTimestamp = string.IsNullOrWhiteSpace(stopAtTimestamp) ? null : DateTimeOffset.Parse(stopAtTimestamp, CultureInfo.InvariantCulture);
+ if (startFromTimestamp is not null && stopAtTimestamp is not null && StartFromTimestamp >= StopAtTimestamp)
+ {
+ throw new ArgumentException("StartFromTimestamp must be before StopAtTimestamp");
+ }
+
Partitioned = partitioned;
Scope = inScope ?? ScopeId.Default;
}
public int Concurrency { get; set; }
- // public ProcessFrom ResetTo { get; }
- // public string? StartFrom { get; }
- // public string? StopAt { get; }
+ public ProcessFrom StartFrom { get; }
+ public DateTimeOffset? StartFromTimestamp { get; }
+ public DateTimeOffset? StopAtTimestamp { get; }
///
/// Gets a value indicating whether this event handler is partitioned.
@@ -67,8 +72,8 @@ public EventHandlerAttribute(
///
public EventHandlerModelId GetIdentifier(Type decoratedType)
{
- DateTimeOffset? startFrom = null;
- DateTimeOffset? stopAt = null;
+ // DateTimeOffset? startFrom = null;
+ // DateTimeOffset? stopAt = null;
// if (!string.IsNullOrWhiteSpace(StartFrom))
// {
// startFrom = DateTimeOffset.Parse(StartFrom, DateTimeFormatInfo.InvariantInfo);
@@ -78,6 +83,6 @@ public EventHandlerModelId GetIdentifier(Type decoratedType)
// stopAt = DateTimeOffset.Parse(StopAt, DateTimeFormatInfo.InvariantInfo);
// }
- return new(_eventHandlerId, Partitioned, Scope, _alias ?? decoratedType.Name, Concurrency, ProcessFrom.First, startFrom, stopAt);
+ return new(_eventHandlerId, Partitioned, Scope, _alias ?? decoratedType.Name, Concurrency, StartFrom, StartFromTimestamp, StopAtTimestamp);
}
}
diff --git a/Source/Events.Handling/EventHandlerModelId.cs b/Source/Events.Handling/EventHandlerModelId.cs
index 19eb12f0..e8ffcf2a 100644
--- a/Source/Events.Handling/EventHandlerModelId.cs
+++ b/Source/Events.Handling/EventHandlerModelId.cs
@@ -27,8 +27,8 @@ public EventHandlerModelId(
bool partitioned,
ScopeId scope,
string? alias,
- int concurrency = 1,
- ProcessFrom resetTo = ProcessFrom.First,
+ int concurrency,
+ ProcessFrom resetTo,
DateTimeOffset? startFrom = null,
DateTimeOffset? stopAt = null)
: base("EventHandler", id, alias, scope)
diff --git a/Source/Events.Handling/Internal/EventHandlerProcessor.cs b/Source/Events.Handling/Internal/EventHandlerProcessor.cs
index 3b0ac7c1..4dcf490c 100644
--- a/Source/Events.Handling/Internal/EventHandlerProcessor.cs
+++ b/Source/Events.Handling/Internal/EventHandlerProcessor.cs
@@ -58,6 +58,10 @@ public override EventHandlerRegistrationRequest RegistrationRequest
registrationRequest.Alias = _eventHandler.Alias!.Value;
}
+ if (_eventHandler.StopAt.HasValue)
+ {
+ registrationRequest.StopAt = Timestamp.FromDateTimeOffset(_eventHandler.StopAt.Value);
+ }
registrationRequest.EventTypes.AddRange(_eventHandler.HandledEvents.Select(_ => _.ToProtobuf()).ToArray());
return registrationRequest;
}
@@ -75,7 +79,7 @@ static StartFrom GetStartFrom(IEventHandler eventHandler)
return new StartFrom
{
- Position = eventHandler.ResetTo == ProcessFrom.Last ? StartFrom.Types.Position.Latest : StartFrom.Types.Position.Start
+ Position = eventHandler.ResetTo == ProcessFrom.Latest ? StartFrom.Types.Position.Latest : StartFrom.Types.Position.Start
};
}
diff --git a/Source/Events.Handling/ProcessFrom.cs b/Source/Events.Handling/ProcessFrom.cs
index 7bbfeccf..9efd5df8 100644
--- a/Source/Events.Handling/ProcessFrom.cs
+++ b/Source/Events.Handling/ProcessFrom.cs
@@ -11,9 +11,9 @@ public enum ProcessFrom
///
/// Start from the beginning of the stream, process all events.
///
- First,
+ Earliest,
///
/// Start after the latest event in the stream, only process new events.
///
- Last
+ Latest
}
diff --git a/Specifications/Events.Handling/for_EventHandler/when_creating.cs b/Specifications/Events.Handling/for_EventHandler/when_creating.cs
index 1958ad9f..c42f6aa5 100644
--- a/Specifications/Events.Handling/for_EventHandler/when_creating.cs
+++ b/Specifications/Events.Handling/for_EventHandler/when_creating.cs
@@ -18,6 +18,8 @@ public class when_creating
static IEnumerable event_types;
static IDictionary event_handler_methods;
static EventHandler event_handler;
+ static int concurrency = 1;
+ static ProcessFrom reset_to = ProcessFrom.Earliest;
Establish context = () =>
{
@@ -36,7 +38,7 @@ public class when_creating
_ => new EventHandlerMethod((@event, eventContext) => Task.CompletedTask) as IEventHandlerMethod));
};
- Because of = () => event_handler = new EventHandler(new EventHandlerModelId(identifier, partitioned, scope_id, alias), event_handler_methods);
+ Because of = () => event_handler = new EventHandler(new EventHandlerModelId(identifier, partitioned, scope_id, alias, concurrency, reset_to), event_handler_methods);
It should_not_be_null = () => event_handler.ShouldNotBeNull();
It should_have_the_correct_partitioned_value = () => event_handler.Partitioned.ShouldEqual(partitioned);
diff --git a/Specifications/Events.Handling/for_EventHandler/when_handling/given/an_event_handler.cs b/Specifications/Events.Handling/for_EventHandler/when_handling/given/an_event_handler.cs
index 7909553b..2dcc3976 100644
--- a/Specifications/Events.Handling/for_EventHandler/when_handling/given/an_event_handler.cs
+++ b/Specifications/Events.Handling/for_EventHandler/when_handling/given/an_event_handler.cs
@@ -17,6 +17,8 @@ public class an_event_handler
protected static Mock event_handler_method;
protected static IDictionary event_handler_methods;
protected static EventHandler event_handler;
+ protected static int concurrency = 1;
+ protected static ProcessFrom reset_to = ProcessFrom.Earliest;
Establish context = () =>
{
@@ -29,6 +31,6 @@ public class an_event_handler
{
{ handled_event_type, event_handler_method.Object }
};
- event_handler = new EventHandler(new EventHandlerModelId(identifier, partitioned, scope_id, "some alias"), event_handler_methods);
+ event_handler = new EventHandler(new EventHandlerModelId(identifier, partitioned, scope_id, "some alias", concurrency, reset_to, null, null), event_handler_methods);
};
}
\ No newline at end of file
diff --git a/Tests/Analyzers/CodeFixProviderTests.cs b/Tests/Analyzers/CodeFixProviderTests.cs
index e1845749..42979749 100644
--- a/Tests/Analyzers/CodeFixProviderTests.cs
+++ b/Tests/Analyzers/CodeFixProviderTests.cs
@@ -18,6 +18,9 @@ public abstract class CodeFixProviderTests : AnalyzerTest
{
TestCode = source,
@@ -38,4 +41,9 @@ protected Task VerifyCodeFixAsync(string source, string expectedResult, Diagnost
return test.RunAsync(CancellationToken.None);
}
+
+ string ToLfLineEndings(string source)
+ {
+ return source.Replace("\r\n", "\n");
+ }
}
diff --git a/Tests/Analyzers/CodeFixes/EventHandlerEventContextCodeFixProviderTests.cs b/Tests/Analyzers/CodeFixes/EventHandlerEventContextCodeFixProviderTests.cs
new file mode 100644
index 00000000..e60f5534
--- /dev/null
+++ b/Tests/Analyzers/CodeFixes/EventHandlerEventContextCodeFixProviderTests.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Dolittle. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Threading.Tasks;
+
+namespace Dolittle.SDK.Analyzers.CodeFixes;
+
+public class EventHandlerEventContextCodeFixProviderTests: CodeFixProviderTests
+{
+
+ [Fact]
+ public async Task ShouldGenerateEventContext()
+ {
+ var test = @"using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+
+[EventType(""86dd35ee-cd28-48d9-a0cd-cb2aa11851aa"")]
+record SomeEvent();
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFrom: ProcessFrom.Latest, stopAtTimestamp: ""2023-09-15T07:30Z"")]
+class SomeEventHandler
+{
+ public void Handle(SomeEvent evt)
+ {
+ System.Console.WriteLine(""Hello World"");
+ }
+}";
+
+ var expected = @"using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+
+[EventType(""86dd35ee-cd28-48d9-a0cd-cb2aa11851aa"")]
+record SomeEvent();
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFrom: ProcessFrom.Latest, stopAtTimestamp: ""2023-09-15T07:30Z"")]
+class SomeEventHandler
+{
+ public void Handle(SomeEvent evt, EventContext ctx)
+ {
+ System.Console.WriteLine(""Hello World"");
+ }
+}";
+
+ var diagnosticResult = Diagnostic(DescriptorRules.Events.MissingEventContext)
+ .WithSpan(10, 17, 10, 23)
+ .WithArguments("Handle");
+
+ await VerifyCodeFixAsync(test, expected, diagnosticResult);
+ }
+
+ [Fact]
+ public async Task ShouldGenerateEventContextAndNamespace()
+ {
+ var test = @"using Dolittle.SDK.Events.Handling;
+
+[Dolittle.SDK.Events.EventType(""86dd35ee-cd28-48d9-a0cd-cb2aa11851aa"")]
+record SomeEvent();
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFrom: ProcessFrom.Latest, stopAtTimestamp: ""2023-09-15T07:30Z"")]
+class SomeEventHandler
+{
+ public void Handle(SomeEvent evt)
+ {
+ System.Console.WriteLine(""Hello World"");
+ }
+}";
+
+ var expected = @"using Dolittle.SDK.Events.Handling;
+using Dolittle.SDK.Events;
+
+[Dolittle.SDK.Events.EventType(""86dd35ee-cd28-48d9-a0cd-cb2aa11851aa"")]
+record SomeEvent();
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFrom: ProcessFrom.Latest, stopAtTimestamp: ""2023-09-15T07:30Z"")]
+class SomeEventHandler
+{
+ public void Handle(SomeEvent evt, EventContext ctx)
+ {
+ System.Console.WriteLine(""Hello World"");
+ }
+}";
+
+ var diagnosticResult = Diagnostic(DescriptorRules.Events.MissingEventContext)
+ .WithSpan(9, 17, 9, 23)
+ .WithArguments("Handle");
+
+ await VerifyCodeFixAsync(test, expected, diagnosticResult);
+ }
+}
diff --git a/Tests/Analyzers/Diagnostics/AggregateAnalyzerTests.cs b/Tests/Analyzers/Diagnostics/AggregateAnalyzerTests.cs
index f922eb4c..f0619389 100644
--- a/Tests/Analyzers/Diagnostics/AggregateAnalyzerTests.cs
+++ b/Tests/Analyzers/Diagnostics/AggregateAnalyzerTests.cs
@@ -22,6 +22,10 @@ record NameUpdated(string Name);
[AggregateRoot(""10ef9f40-3e61-444a-9601-f521be2d547e"")]
class SomeAggregate: AggregateRoot
{
+ public SomeAggregate(){
+ Name = ""John Doe"";
+ }
+
public string Name {get; set;}
public void UpdateName(string name)
@@ -38,7 +42,7 @@ private void On(NameUpdated @event)
}
[Fact]
- public async Task ShouldFindNonPrivateMutation()
+ public async Task ShouldFindNonPrivateOnMethod()
{
var test = @"
using Dolittle.SDK.Aggregates;
@@ -75,7 +79,7 @@ public void On(NameUpdated @event)
}
[Fact]
- public async Task ShouldFindMutationWithIncorrectParameters()
+ public async Task ShouldFindOnMethodWithIncorrectParameters()
{
var test = @"
using Dolittle.SDK.Aggregates;
@@ -112,7 +116,7 @@ private void On(NameUpdated @event, string shouldNotBeHere)
}
[Fact]
- public async Task ShouldFindMutationWithNoParameters()
+ public async Task ShouldFindOnMethodWithNoParameters()
{
var test = @"
using Dolittle.SDK.Aggregates;
@@ -225,7 +229,7 @@ private void On(NameUpdated @event)
}
[Fact]
- public async Task ShouldFindMissingMutationFromConstructor()
+ public async Task ShouldFindMissingOnMethodFromConstructor()
{
var test = @"
using Dolittle.SDK.Aggregates;
@@ -256,7 +260,7 @@ public void UpdateName(string name)
}
[Fact]
- public async Task ShouldFindMissingMutationWhenNested()
+ public async Task ShouldFindMissingOnMethodWhenNested()
{
var test = @"
using Dolittle.SDK.Aggregates;
@@ -290,7 +294,7 @@ public void UpdateName(string name)
}
[Fact]
- public async Task ShouldFindMissingMutation()
+ public async Task ShouldFindMissingOnMethod()
{
var test = @"
using Dolittle.SDK.Aggregates;
@@ -328,7 +332,7 @@ public void Rename(string name)
}
[Fact]
- public async Task ShouldFindMissingMutationFromVariable()
+ public async Task ShouldFindMissingOnMethodFromVariable()
{
var test = @"
using Dolittle.SDK.Aggregates;
@@ -534,4 +538,158 @@ void On(NameUpdated @event)
await VerifyAnalyzerFindsNothingAsync(test);
}
+ [Fact]
+ public async Task ShouldFindInvalidMutationOnProperty()
+ {
+ var test = @"
+using Dolittle.SDK.Aggregates;
+using Dolittle.SDK.Events;
+
+
+[EventType(""5dc02e84-c6fc-4e1b-997c-ec33d0048a3b"")]
+record NameUpdated(string Name);
+
+[AggregateRoot(""10ef9f40-3e61-444a-9601-f521be2d547e"")]
+class SomeAggregate: AggregateRoot
+{
+ public string Name {get; set;}
+
+ public void UpdateName(string name)
+ {
+ Apply(new NameUpdated(name));
+ Name = name;
+ }
+
+ private void On(NameUpdated @event)
+ {
+ Name = @event.Name;
+ }
+}";
+
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.Aggregate.PublicMethodsCannotMutateAggregateState)
+ .WithSpan(17, 9, 17, 13)
+ .WithArguments("Name = name;")
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
+
+
+ [Fact]
+ public async Task ShouldFindInvalidMutationOnThisProperty()
+ {
+ var test = @"
+using Dolittle.SDK.Aggregates;
+using Dolittle.SDK.Events;
+
+
+[EventType(""5dc02e84-c6fc-4e1b-997c-ec33d0048a3b"")]
+record NameUpdated(string Name);
+
+[AggregateRoot(""10ef9f40-3e61-444a-9601-f521be2d547e"")]
+class SomeAggregate: AggregateRoot
+{
+ public string Name {get; set;}
+
+ public void UpdateName(string name)
+ {
+ Apply(new NameUpdated(name));
+ this.Name = name;
+ }
+
+ private void On(NameUpdated @event)
+ {
+ Name = @event.Name;
+ }
+}";
+
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.Aggregate.PublicMethodsCannotMutateAggregateState)
+ .WithSpan(17, 9, 17, 18)
+ .WithArguments("this.Name = name;")
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
+
+ [Fact]
+ public async Task ShouldFindInvalidMutationOnField()
+ {
+ var test = @"
+using Dolittle.SDK.Aggregates;
+using Dolittle.SDK.Events;
+
+
+[EventType(""5dc02e84-c6fc-4e1b-997c-ec33d0048a3b"")]
+record NameUpdated(string Name);
+
+[AggregateRoot(""10ef9f40-3e61-444a-9601-f521be2d547e"")]
+class SomeAggregate: AggregateRoot
+{
+ string? _name;
+
+ public void UpdateName(string name)
+ {
+ Apply(new NameUpdated(name));
+ _name = name;
+ }
+
+ private void On(NameUpdated @event)
+ {
+ _name = @event.Name;
+ }
+}";
+
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.Aggregate.PublicMethodsCannotMutateAggregateState)
+ .WithSpan(17, 9, 17, 14)
+ .WithArguments("_name = name;")
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
+
+
+ [Fact]
+ public async Task ShouldFindInvalidMutationOnThisField()
+ {
+ var test = @"
+using Dolittle.SDK.Aggregates;
+using Dolittle.SDK.Events;
+
+
+[EventType(""5dc02e84-c6fc-4e1b-997c-ec33d0048a3b"")]
+record NameUpdated(string Name);
+
+[AggregateRoot(""10ef9f40-3e61-444a-9601-f521be2d547e"")]
+class SomeAggregate: AggregateRoot
+{
+ string? _name;
+
+ public void UpdateName(string name)
+ {
+ Apply(new NameUpdated(name));
+ this._name = name;
+ }
+
+ private void On(NameUpdated @event)
+ {
+ this._name = @event.Name;
+ }
+}";
+
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.Aggregate.PublicMethodsCannotMutateAggregateState)
+ .WithSpan(17, 9, 17, 19)
+ .WithArguments("this._name = name;")
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
+
}
diff --git a/Tests/Analyzers/Diagnostics/AnnotationAnalyzerTests.cs b/Tests/Analyzers/Diagnostics/AnnotationAnalyzerTests.cs
index 0b5a1172..35071406 100644
--- a/Tests/Analyzers/Diagnostics/AnnotationAnalyzerTests.cs
+++ b/Tests/Analyzers/Diagnostics/AnnotationAnalyzerTests.cs
@@ -134,4 +134,23 @@ class SomeEvent
await VerifyAnalyzerFindsNothingAsync(test);
}
+
+ [Fact]
+ public async Task ShouldDetectEventHandlerWhenQualified()
+ {
+ var test = @"
+[Dolittle.SDK.Events.Handling.EventHandler("""")]
+class SomeEvent
+{
+ public string Name {get; set;}
+}";
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.InvalidIdentity)
+ .WithSpan(2, 2, 2, 47)
+ .WithArguments("Dolittle.SDK.Events.Handling.EventHandler", "eventHandlerId", "\"\""),
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
}
diff --git a/Tests/Analyzers/Diagnostics/EventHandlerAnalyzerTests.cs b/Tests/Analyzers/Diagnostics/EventHandlerAnalyzerTests.cs
new file mode 100644
index 00000000..ebb3b896
--- /dev/null
+++ b/Tests/Analyzers/Diagnostics/EventHandlerAnalyzerTests.cs
@@ -0,0 +1,269 @@
+// Copyright (c) Dolittle. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+
+using System.Threading.Tasks;
+
+namespace Dolittle.SDK.Analyzers.Diagnostics;
+
+public class EventHandlerAnalyzerTests : AnalyzerTest
+{
+ [Fact]
+ public async Task ShouldDetectNothing()
+ {
+ var test = @"
+using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+
+[EventType(""86dd35ee-cd28-48d9-a0cd-cb2aa11851aa"")]
+record SomeEvent();
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFrom: ProcessFrom.Latest, stopAtTimestamp:""2023-09-15T07:30Z"")]
+class SomeEventHandler
+{
+ public void Handle(SomeEvent evt, EventContext context)
+ {
+ }
+}";
+ await VerifyAnalyzerFindsNothingAsync(test);
+ }
+
+ [Fact]
+ public async Task ShouldDetectNothingAsync()
+ {
+ var test = @"
+using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+using System.Threading.Tasks;
+
+[EventType(""86dd35ee-cd28-48d9-a0cd-cb2aa11851aa"")]
+record SomeEvent();
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFrom: ProcessFrom.Latest, stopAtTimestamp:""2023-09-15T07:30Z"")]
+class SomeEventHandler
+{
+ public async Task Handle(SomeEvent evt, EventContext context)
+ {
+ }
+}";
+ await VerifyAnalyzerFindsNothingAsync(test);
+ }
+
+ [Fact]
+ public async Task ShouldDetectInvalidStartFromTimestamp()
+ {
+ var test = @"
+using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFrom: ProcessFrom.Latest, startFromTimestamp:""2023-09-15T07:30Z!"")]
+class SomeEventHandler
+{
+
+}";
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.InvalidTimestamp)
+ .WithSpan(5, 86, 5, 125)
+ .WithArguments("startFromTimestamp"),
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
+
+ [Fact]
+ public async Task ShouldDetectInvalidStopAtTimestamp()
+ {
+ var test = @"
+using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFrom: ProcessFrom.Latest, stopAtTimestamp:""2023-09-15T07:30Z!"")]
+class SomeEventHandler
+{
+
+}";
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.InvalidTimestamp)
+ .WithSpan(5, 86, 5, 122)
+ .WithArguments("stopAtTimestamp"),
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
+
+ [Fact]
+ public async Task ShouldNotDetectDetectTimestampIssues()
+ {
+ var test = @"
+using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFromTimestamp:""2023-09-15T06:25Z"", stopAtTimestamp:""2023-09-15T06:30Z"")]
+class SomeEventHandler
+{
+
+}";
+
+ await VerifyAnalyzerFindsNothingAsync(test);
+
+ }
+
+ [Fact]
+ public async Task ShouldDetectDetectTimestampIssues()
+ {
+ var test = @"
+using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", stopAtTimestamp:""2023-09-15T06:25Z"", startFromTimestamp:""2023-09-15T06:30Z"")]
+class SomeEventHandler
+{
+
+}";
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.InvalidStartStopTimestamp)
+ .WithSpan(5, 2, 5, 131)
+ .WithArguments("startFromTimestamp","stopAtTimestamp"),
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
+
+ [Fact]
+ public async Task ShouldDetectDetectMissingEventTypeAnnotation()
+ {
+ var test = @"
+using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+
+record SomeEvent();
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"")]
+class SomeEventHandler
+{
+ public void Handle(SomeEvent evt, EventContext context)
+ {
+ }
+}";
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.Events.MissingAttribute)
+ .WithSpan(10, 34, 10, 37)
+ .WithArguments("SomeEvent"),
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
+
+ [Fact]
+ public async Task ShouldDetectInvalidAccessibilityWhenNotSpecific()
+ {
+ var test = @"
+using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+
+[EventType(""86dd35ee-cd28-48d9-a0cd-cb2aa11851aa"")]
+record SomeEvent();
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFrom: ProcessFrom.Latest, stopAtTimestamp:""2023-09-15T07:30Z"")]
+class SomeEventHandler
+{
+ void Handle(SomeEvent evt, EventContext context)
+ {
+ }
+}";
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.InvalidAccessibility)
+ .WithSpan(11, 10, 11, 16)
+ .WithArguments("Handle", "Public"),
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
+
+ [Fact]
+ public async Task ShouldDetectInvalidAccessibilityWhenPrivate()
+ {
+ var test = @"
+using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+
+[EventType(""86dd35ee-cd28-48d9-a0cd-cb2aa11851aa"")]
+record SomeEvent();
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFrom: ProcessFrom.Latest, stopAtTimestamp:""2023-09-15T07:30Z"")]
+class SomeEventHandler
+{
+ private void Handle(SomeEvent evt, EventContext context)
+ {
+ }
+}";
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.InvalidAccessibility)
+ .WithSpan(11, 18, 11, 24)
+ .WithArguments("Handle", "Public"),
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
+
+ [Fact]
+ public async Task ShouldDetectInvalidAccessibilityWhenInternal()
+ {
+ var test = @"
+using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+
+[EventType(""86dd35ee-cd28-48d9-a0cd-cb2aa11851aa"")]
+record SomeEvent();
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFrom: ProcessFrom.Latest, stopAtTimestamp:""2023-09-15T07:30Z"")]
+class SomeEventHandler
+{
+ internal void Handle(SomeEvent evt, EventContext context)
+ {
+ }
+}";
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.InvalidAccessibility)
+ .WithSpan(11, 19, 11, 25)
+ .WithArguments("Handle", "Public"),
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
+
+ [Fact]
+ public async Task ShouldDetectMissingEventContext()
+ {
+ var test = @"
+using Dolittle.SDK.Events;
+using Dolittle.SDK.Events.Handling;
+
+[EventType(""86dd35ee-cd28-48d9-a0cd-cb2aa11851aa"")]
+record SomeEvent();
+
+[EventHandler(""86dd35ee-cd28-48d9-a0cd-cb2aa11851af"", startFrom: ProcessFrom.Latest, stopAtTimestamp:""2023-09-15T07:30Z"")]
+class SomeEventHandler
+{
+ public void Handle(SomeEvent evt)
+ {
+ }
+}";
+ DiagnosticResult[] expected =
+ {
+ Diagnostic(DescriptorRules.Events.MissingEventContext)
+ .WithSpan(11, 17, 11, 23)
+ .WithArguments("Handle"),
+ };
+
+ await VerifyAnalyzerAsync(test, expected);
+ }
+
+}