Skip to content
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
97 changes: 61 additions & 36 deletions src/BuildingBlocks/Eventing/InMemory/InMemoryEventBus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,58 +42,83 @@ private async Task PublishSingleAsync(IIntegrationEvent @event, CancellationToke
using var scope = _serviceProvider.CreateScope();
var provider = scope.ServiceProvider;

var handlerInterfaceType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
var handlers = provider.GetServices(handlerInterfaceType).ToArray();

var handlers = ResolveHandlers(provider, eventType);
if (handlers.Length == 0)
{
_logger.LogDebug("No handlers registered for integration event type {EventType}", eventType.FullName);
return;
}

var inbox = provider.GetService<IInboxStore>();
var handlerInterfaceType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);

foreach (var handler in handlers)
{
if (handler is null)
{
continue;
}
await InvokeHandlerAsync(handler, handlerInterfaceType, eventType, @event, inbox, ct);
}
}

var handlerName = handler.GetType().FullName ?? handler.GetType().Name;
private static object[] ResolveHandlers(IServiceProvider provider, Type eventType)
{
var handlerInterfaceType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
return provider.GetServices(handlerInterfaceType).Where(h => h is not null).ToArray()!;
}

if (inbox != null)
{
if (await inbox.HasProcessedAsync(@event.Id, handlerName, ct).ConfigureAwait(false))
{
_logger.LogDebug("Skipping already processed integration event {EventId} for handler {Handler}", @event.Id, handlerName);
continue;
}
}
private async Task InvokeHandlerAsync(
object handler,
Type handlerInterfaceType,
Type eventType,
IIntegrationEvent @event,
IInboxStore? inbox,
CancellationToken ct)
{
var handlerName = handler.GetType().FullName ?? handler.GetType().Name;

var method = handlerInterfaceType.GetMethod(nameof(IIntegrationEventHandler<IIntegrationEvent>.HandleAsync));
if (method == null)
{
_logger.LogWarning("Handler {Handler} does not implement HandleAsync correctly for {EventType}", handlerName, eventType.FullName);
continue;
}
if (await ShouldSkipProcessedEventAsync(inbox, @event.Id, handlerName, ct))
{
_logger.LogDebug("Skipping already processed integration event {EventId} for handler {Handler}", @event.Id, handlerName);
return;
}

try
{
var task = (Task)method.Invoke(handler, new object[] { @event, ct })!;
await task.ConfigureAwait(false);

if (inbox != null)
{
await inbox.MarkProcessedAsync(@event.Id, handlerName, @event.TenantId, eventType.AssemblyQualifiedName ?? eventType.FullName!, ct)
.ConfigureAwait(false);
}
}
catch (Exception ex)
var method = handlerInterfaceType.GetMethod(nameof(IIntegrationEventHandler<IIntegrationEvent>.HandleAsync));
if (method == null)
{
_logger.LogWarning("Handler {Handler} does not implement HandleAsync correctly for {EventType}", handlerName, eventType.FullName);
return;
}

await ExecuteHandlerAsync(handler, method, @event, eventType, handlerName, inbox, ct);
}

private static async Task<bool> ShouldSkipProcessedEventAsync(IInboxStore? inbox, Guid eventId, string handlerName, CancellationToken ct)
{
return inbox != null && await inbox.HasProcessedAsync(eventId, handlerName, ct).ConfigureAwait(false);
}

private async Task ExecuteHandlerAsync(
object handler,
MethodInfo method,
IIntegrationEvent @event,
Type eventType,
string handlerName,
IInboxStore? inbox,
CancellationToken ct)
{
try
{
var task = (Task)method.Invoke(handler, new object[] { @event, ct })!;
await task.ConfigureAwait(false);

if (inbox != null)
{
_logger.LogError(ex, "Error while handling integration event {EventId} with handler {Handler}", @event.Id, handlerName);
throw;
await inbox.MarkProcessedAsync(@event.Id, handlerName, @event.TenantId, eventType.AssemblyQualifiedName ?? eventType.FullName!, ct)
.ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while handling integration event {EventId} with handler {Handler}", @event.Id, handlerName);
throw;
}
}
}
51 changes: 39 additions & 12 deletions src/BuildingBlocks/Mailing/Services/SendGridMailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,61 @@ public SendGridMailService(IOptions<MailOptions> settings)
public async Task SendAsync(MailRequest request, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(request);
if (_settings.SendGrid?.ApiKey is null)
throw new InvalidOperationException("SendGrid ApiKey is not configured.");
ValidateConfiguration();

var client = new SendGridClient(_settings.SendGrid.ApiKey);
var from = new EmailAddress(request.From ?? _settings.SendGrid.From ?? _settings.From, request.DisplayName ?? _settings.SendGrid.DisplayName ?? _settings.DisplayName);
var client = new SendGridClient(_settings.SendGrid!.ApiKey!);
var from = CreateFromAddress(request);
var msg = MailHelper.CreateSingleEmail(
from,
new EmailAddress(request.To[0]),
request.Subject,
request.Body,
request.Body);

ConfigureRecipients(msg, request);
AddAttachments(msg, request);

await client.SendEmailAsync(msg, ct);
}

private void ValidateConfiguration()
{
if (_settings.SendGrid?.ApiKey is null)
{
throw new InvalidOperationException("SendGrid ApiKey is not configured.");
}
}

private EmailAddress CreateFromAddress(MailRequest request)
{
var email = request.From ?? _settings.SendGrid?.From ?? _settings.From;
var displayName = request.DisplayName ?? _settings.SendGrid?.DisplayName ?? _settings.DisplayName;
return new EmailAddress(email, displayName);
}

private static void ConfigureRecipients(SendGridMessage msg, MailRequest request)
{
if (request.Cc.Count > 0)
{
msg.AddCcs(request.Cc.Select(cc => new EmailAddress(cc)).ToList());
}

if (request.Bcc.Count > 0)
{
msg.AddBccs(request.Bcc.Select(bcc => new EmailAddress(bcc)).ToList());
}

if (request.ReplyTo != null)
{
msg.ReplyTo = new EmailAddress(request.ReplyTo, request.ReplyToName);
}
}

// Attachments
if (request.AttachmentData.Count > 0)
private static void AddAttachments(SendGridMessage msg, MailRequest request)
{
foreach (var att in request.AttachmentData)
{
foreach (var att in request.AttachmentData)
{
msg.AddAttachment(att.Key, Convert.ToBase64String(att.Value));
}
msg.AddAttachment(att.Key, Convert.ToBase64String(att.Value));
}

await client.SendEmailAsync(msg, ct);
}
}
89 changes: 41 additions & 48 deletions src/BuildingBlocks/Persistence/Specifications/Specification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,75 +153,68 @@ protected void ApplySortingOverride(
ArgumentNullException.ThrowIfNull(applyDefaultOrdering);
ArgumentNullException.ThrowIfNull(sortMappings);

ClearOrderExpressions();

if (string.IsNullOrWhiteSpace(sortExpression))
{
ClearOrderExpressions();
applyDefaultOrdering();
return;
}

ClearOrderExpressions();
var clauses = ParseSortClauses(sortExpression);
bool anyApplied = ApplySortClauses(clauses, sortMappings);

string[] clauses = sortExpression.Split(
',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (!anyApplied)
{
applyDefaultOrdering();
}
}

private static IEnumerable<string> ParseSortClauses(string sortExpression)
{
return sortExpression.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(clause => !string.IsNullOrWhiteSpace(clause));
}

private bool ApplySortClauses(IEnumerable<string> clauses, IReadOnlyDictionary<string, Expression<Func<T, object>>> sortMappings)
{
bool anyApplied = false;

foreach (string rawClause in clauses)
{
if (string.IsNullOrWhiteSpace(rawClause))
var (key, descending) = ParseSortClause(rawClause);

if (string.IsNullOrWhiteSpace(key) || !sortMappings.TryGetValue(key, out var selector))
{
continue;
}

string clause = rawClause.Trim();
bool descending = clause[0] == '-';
if (clause[0] is '-' or '+')
{
clause = clause[1..];
}
ApplySortOrder(selector, descending, anyApplied);
anyApplied = true;
}

if (string.IsNullOrWhiteSpace(clause))
{
continue;
}
return anyApplied;
}

if (!sortMappings.TryGetValue(clause, out Expression<Func<T, object>>? selector))
{
// Unknown sort key; skip to keep sorting safe.
continue;
}
private static (string key, bool descending) ParseSortClause(string clause)
{
clause = clause.Trim();
bool descending = clause[0] == '-';
string key = clause[0] is '-' or '+' ? clause[1..] : clause;
return (key, descending);
}

if (!anyApplied)
{
if (descending)
{
OrderByDescending(selector);
}
else
{
OrderBy(selector);
}

anyApplied = true;
}
else
{
if (descending)
{
ThenByDescending(selector);
}
else
{
ThenBy(selector);
}
}
private void ApplySortOrder(Expression<Func<T, object>> selector, bool descending, bool isSecondary)
{
if (isSecondary)
{
if (descending) ThenByDescending(selector);
else ThenBy(selector);
}

if (!anyApplied)
else
{
applyDefaultOrdering();
if (descending) OrderByDescending(selector);
else OrderBy(selector);
}
}

Expand Down
Loading
Loading