feat(notification-outbox): add AddNotificationOutbox DI registration

This commit is contained in:
Joseph Doherty
2026-05-19 02:07:29 -04:00
parent 517437b0d9
commit 703cb2d392
8 changed files with 218 additions and 45 deletions

View File

@@ -31,7 +31,6 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly NotificationOutboxOptions _options; private readonly NotificationOutboxOptions _options;
private readonly ILogger<NotificationOutboxActor> _logger; private readonly ILogger<NotificationOutboxActor> _logger;
private readonly IReadOnlyDictionary<NotificationType, INotificationDeliveryAdapter> _adapters;
/// <summary> /// <summary>
/// In-flight guard for the dispatch loop. Set true at the start of a sweep and cleared /// In-flight guard for the dispatch loop. Set true at the start of a sweep and cleared
@@ -46,13 +45,11 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
public NotificationOutboxActor( public NotificationOutboxActor(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
NotificationOutboxOptions options, NotificationOutboxOptions options,
ILogger<NotificationOutboxActor> logger, ILogger<NotificationOutboxActor> logger)
IReadOnlyDictionary<NotificationType, INotificationDeliveryAdapter> adapters)
{ {
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_options = options; _options = options;
_logger = logger; _logger = logger;
_adapters = adapters;
Receive<NotificationSubmit>(HandleSubmit); Receive<NotificationSubmit>(HandleSubmit);
Receive<InternalMessages.IngestPersisted>(HandleIngestPersisted); Receive<InternalMessages.IngestPersisted>(HandleIngestPersisted);
@@ -174,6 +171,11 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
/// in a try/catch so the returned task never faults — scope creation, service resolution, /// in a try/catch so the returned task never faults — scope creation, service resolution,
/// and retry-policy resolution can all throw, and a faulted task would otherwise leave /// and retry-policy resolution can all throw, and a faulted task would otherwise leave
/// the dispatcher's in-flight guard stuck and wedge the loop permanently. /// the dispatcher's in-flight guard stuck and wedge the loop permanently.
///
/// The channel delivery adapters are resolved from the per-sweep scope, not held in a
/// field: <see cref="EmailNotificationDeliveryAdapter"/> takes a scoped
/// <see cref="INotificationRepository"/> directly, so a long-lived adapter reference on
/// this singleton actor would be a captive dependency over a disposed DbContext.
/// </summary> /// </summary>
private async Task RunDispatchPass(DateTimeOffset now) private async Task RunDispatchPass(DateTimeOffset now)
{ {
@@ -182,6 +184,7 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
using var scope = _serviceProvider.CreateScope(); using var scope = _serviceProvider.CreateScope();
var outboxRepository = scope.ServiceProvider.GetRequiredService<INotificationOutboxRepository>(); var outboxRepository = scope.ServiceProvider.GetRequiredService<INotificationOutboxRepository>();
var notificationRepository = scope.ServiceProvider.GetRequiredService<INotificationRepository>(); var notificationRepository = scope.ServiceProvider.GetRequiredService<INotificationRepository>();
var adapters = ResolveAdapters(scope.ServiceProvider);
IReadOnlyList<Notification> due; IReadOnlyList<Notification> due;
try try
@@ -205,7 +208,7 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
{ {
try try
{ {
await DeliverOneAsync(notification, now, maxRetries, retryDelay, outboxRepository); await DeliverOneAsync(notification, now, maxRetries, retryDelay, outboxRepository, adapters);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -238,6 +241,24 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
: (configuration.MaxRetries, configuration.RetryDelay); : (configuration.MaxRetries, configuration.RetryDelay);
} }
/// <summary>
/// Builds the <see cref="NotificationType"/> → adapter lookup for a dispatch sweep from
/// the registered <see cref="INotificationDeliveryAdapter"/> services in the supplied
/// scope. The last adapter registered for a given type wins, mirroring DI's last-wins
/// resolution semantics.
/// </summary>
private static IReadOnlyDictionary<NotificationType, INotificationDeliveryAdapter> ResolveAdapters(
IServiceProvider scopedServices)
{
var adapters = new Dictionary<NotificationType, INotificationDeliveryAdapter>();
foreach (var adapter in scopedServices.GetServices<INotificationDeliveryAdapter>())
{
adapters[adapter.Type] = adapter;
}
return adapters;
}
/// <summary> /// <summary>
/// Delivers a single notification through its channel adapter and applies the resulting /// Delivers a single notification through its channel adapter and applies the resulting
/// status transition. A missing adapter parks the notification; otherwise the /// status transition. A missing adapter parks the notification; otherwise the
@@ -248,9 +269,10 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
DateTimeOffset now, DateTimeOffset now,
int maxRetries, int maxRetries,
TimeSpan retryDelay, TimeSpan retryDelay,
INotificationOutboxRepository outboxRepository) INotificationOutboxRepository outboxRepository,
IReadOnlyDictionary<NotificationType, INotificationDeliveryAdapter> adapters)
{ {
if (!_adapters.TryGetValue(notification.Type, out var adapter)) if (!adapters.TryGetValue(notification.Type, out var adapter))
{ {
notification.Status = NotificationStatus.Parked; notification.Status = NotificationStatus.Parked;
notification.LastError = $"no delivery adapter for type {notification.Type}"; notification.LastError = $"no delivery adapter for type {notification.Type}";

View File

@@ -0,0 +1,49 @@
using Microsoft.Extensions.DependencyInjection;
using ScadaLink.NotificationOutbox.Delivery;
namespace ScadaLink.NotificationOutbox;
/// <summary>
/// DI registration for the Notification Outbox component: binds
/// <see cref="NotificationOutboxOptions"/> and registers the channel delivery adapters.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>Configuration section bound to <see cref="NotificationOutboxOptions"/>.</summary>
public const string OptionsSection = "ScadaLink:NotificationOutbox";
/// <summary>
/// Registers the Notification Outbox services: the <see cref="NotificationOutboxOptions"/>
/// binding and the channel delivery adapters.
///
/// This extension covers only the outbox-specific registrations. The
/// <see cref="EmailNotificationDeliveryAdapter"/> reuses the
/// <see cref="ScadaLink.NotificationService"/> SMTP machinery —
/// <c>Func&lt;ISmtpClientWrapper&gt;</c>, <c>OAuth2TokenService</c> and
/// <c>NotificationOptions</c> — so the caller (the Host on the central node) must also
/// call <c>AddNotificationService()</c>. Re-registering those services here would
/// duplicate them; relying on <c>AddNotificationService</c> keeps a single source of truth.
///
/// <see cref="EmailNotificationDeliveryAdapter"/> is registered <em>scoped</em> because it
/// takes a scoped <see cref="ScadaLink.Commons.Interfaces.Repositories.INotificationRepository"/>
/// directly. The <see cref="NotificationOutboxActor"/> resolves the adapters from a fresh
/// scope per dispatch sweep rather than holding them, so no scoped adapter is captured by
/// the singleton actor.
/// </summary>
public static IServiceCollection AddNotificationOutbox(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
services.AddOptions<NotificationOutboxOptions>()
.BindConfiguration(OptionsSection);
// Scoped: the adapter holds a scoped INotificationRepository. Registered both under
// the interface (so the dispatch sweep can enumerate every channel adapter) and as
// the concrete type (so callers and tests can resolve it directly).
services.AddScoped<EmailNotificationDeliveryAdapter>();
services.AddScoped<INotificationDeliveryAdapter>(
sp => sp.GetRequiredService<EmailNotificationDeliveryAdapter>());
return services;
}
}

View File

@@ -26,11 +26,19 @@ public class NotificationOutboxActorDispatchTests : TestKit
private readonly INotificationRepository _notificationRepository = private readonly INotificationRepository _notificationRepository =
Substitute.For<INotificationRepository>(); Substitute.For<INotificationRepository>();
private IServiceProvider BuildServiceProvider() private IServiceProvider BuildServiceProvider(
IEnumerable<INotificationDeliveryAdapter> adapters)
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddScoped(_ => _outboxRepository); services.AddScoped(_ => _outboxRepository);
services.AddScoped(_ => _notificationRepository); services.AddScoped(_ => _notificationRepository);
// The actor resolves the channel adapters from its per-sweep DI scope; register
// each stub adapter under the INotificationDeliveryAdapter service.
foreach (var adapter in adapters)
{
services.AddScoped<INotificationDeliveryAdapter>(_ => adapter);
}
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }
@@ -67,14 +75,13 @@ public class NotificationOutboxActorDispatchTests : TestKit
} }
private IActorRef CreateActor( private IActorRef CreateActor(
IReadOnlyDictionary<NotificationType, INotificationDeliveryAdapter> adapters, IEnumerable<INotificationDeliveryAdapter> adapters,
NotificationOutboxOptions? options = null) NotificationOutboxOptions? options = null)
{ {
return Sys.ActorOf(Props.Create(() => new NotificationOutboxActor( return Sys.ActorOf(Props.Create(() => new NotificationOutboxActor(
BuildServiceProvider(), BuildServiceProvider(adapters),
options ?? new NotificationOutboxOptions { DispatchInterval = TimeSpan.FromHours(1) }, options ?? new NotificationOutboxOptions { DispatchInterval = TimeSpan.FromHours(1) },
NullLogger<NotificationOutboxActor>.Instance, NullLogger<NotificationOutboxActor>.Instance)));
adapters)));
} }
private static Notification MakeNotification( private static Notification MakeNotification(
@@ -107,10 +114,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>()) _outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification }); .Returns(new[] { notification });
var adapter = new StubAdapter(() => DeliveryOutcome.Success("ops@example.com")); var adapter = new StubAdapter(() => DeliveryOutcome.Success("ops@example.com"));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter> var actor = CreateActor([adapter]);
{
[NotificationType.Email] = adapter,
});
actor.Tell(InternalMessages.DispatchTick.Instance); actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -130,10 +134,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>()) _outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification }); .Returns(new[] { notification });
var adapter = new StubAdapter(() => DeliveryOutcome.Success("ops@example.com")); var adapter = new StubAdapter(() => DeliveryOutcome.Success("ops@example.com"));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter> var actor = CreateActor([adapter]);
{
[NotificationType.Email] = adapter,
});
actor.Tell(InternalMessages.DispatchTick.Instance); actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -158,10 +159,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>()) _outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification }); .Returns(new[] { notification });
var adapter = new StubAdapter(() => DeliveryOutcome.Transient("smtp timeout")); var adapter = new StubAdapter(() => DeliveryOutcome.Transient("smtp timeout"));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter> var actor = CreateActor([adapter]);
{
[NotificationType.Email] = adapter,
});
actor.Tell(InternalMessages.DispatchTick.Instance); actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -187,10 +185,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>()) _outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification }); .Returns(new[] { notification });
var adapter = new StubAdapter(() => DeliveryOutcome.Transient("smtp timeout")); var adapter = new StubAdapter(() => DeliveryOutcome.Transient("smtp timeout"));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter> var actor = CreateActor([adapter]);
{
[NotificationType.Email] = adapter,
});
actor.Tell(InternalMessages.DispatchTick.Instance); actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -212,10 +207,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>()) _outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification }); .Returns(new[] { notification });
var adapter = new StubAdapter(() => DeliveryOutcome.Permanent("invalid recipient address")); var adapter = new StubAdapter(() => DeliveryOutcome.Permanent("invalid recipient address"));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter> var actor = CreateActor([adapter]);
{
[NotificationType.Email] = adapter,
});
actor.Tell(InternalMessages.DispatchTick.Instance); actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -237,8 +229,8 @@ public class NotificationOutboxActorDispatchTests : TestKit
var notification = MakeNotification(); var notification = MakeNotification();
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>()) _outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification }); .Returns(new[] { notification });
// Empty adapter dictionary: no adapter resolves for the notification's type. // No adapters registered: none resolves for the notification's type.
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter>()); var actor = CreateActor([]);
actor.Tell(InternalMessages.DispatchTick.Instance); actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -262,7 +254,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
// failure were not handled, which would leave _dispatching stuck true forever. // failure were not handled, which would leave _dispatching stuck true forever.
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>()) _outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns<IReadOnlyList<Notification>>(_ => throw new InvalidOperationException("db down")); .Returns<IReadOnlyList<Notification>>(_ => throw new InvalidOperationException("db down"));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter>()); var actor = CreateActor([]);
// First tick: the pass faults internally but must still clear the in-flight guard. // First tick: the pass faults internally but must still clear the in-flight guard.
actor.Tell(InternalMessages.DispatchTick.Instance); actor.Tell(InternalMessages.DispatchTick.Instance);
@@ -287,10 +279,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
var adapter = new StubAdapter( var adapter = new StubAdapter(
() => DeliveryOutcome.Success("ops@example.com"), () => DeliveryOutcome.Success("ops@example.com"),
delay: TimeSpan.FromMilliseconds(800)); delay: TimeSpan.FromMilliseconds(800));
var actor = CreateActor(new Dictionary<NotificationType, INotificationDeliveryAdapter> var actor = CreateActor([adapter]);
{
[NotificationType.Email] = adapter,
});
actor.Tell(InternalMessages.DispatchTick.Instance); actor.Tell(InternalMessages.DispatchTick.Instance);
actor.Tell(InternalMessages.DispatchTick.Instance); actor.Tell(InternalMessages.DispatchTick.Instance);

View File

@@ -34,8 +34,7 @@ public class NotificationOutboxActorIngestTests : TestKit
return Sys.ActorOf(Props.Create(() => new NotificationOutboxActor( return Sys.ActorOf(Props.Create(() => new NotificationOutboxActor(
BuildServiceProvider(), BuildServiceProvider(),
new NotificationOutboxOptions(), new NotificationOutboxOptions(),
NullLogger<NotificationOutboxActor>.Instance, NullLogger<NotificationOutboxActor>.Instance)));
new Dictionary<NotificationType, INotificationDeliveryAdapter>())));
} }
private static NotificationSubmit MakeSubmit(string? notificationId = null) private static NotificationSubmit MakeSubmit(string? notificationId = null)

View File

@@ -46,8 +46,7 @@ public class NotificationOutboxActorPurgeTests : TestKit
DispatchInterval = TimeSpan.FromHours(1), DispatchInterval = TimeSpan.FromHours(1),
PurgeInterval = TimeSpan.FromHours(1), PurgeInterval = TimeSpan.FromHours(1),
}, },
NullLogger<NotificationOutboxActor>.Instance, NullLogger<NotificationOutboxActor>.Instance)));
new Dictionary<NotificationType, INotificationDeliveryAdapter>())));
} }
[Fact] [Fact]

View File

@@ -36,8 +36,7 @@ public class NotificationOutboxActorQueryTests : TestKit
BuildServiceProvider(), BuildServiceProvider(),
// A long dispatch interval keeps the dispatch loop from interfering with these tests. // A long dispatch interval keeps the dispatch loop from interfering with these tests.
options ?? new NotificationOutboxOptions { DispatchInterval = TimeSpan.FromHours(1) }, options ?? new NotificationOutboxOptions { DispatchInterval = TimeSpan.FromHours(1) },
NullLogger<NotificationOutboxActor>.Instance, NullLogger<NotificationOutboxActor>.Instance)));
new Dictionary<NotificationType, INotificationDeliveryAdapter>())));
} }
private static Notification MakeNotification( private static Notification MakeNotification(

View File

@@ -11,6 +11,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Akka.TestKit.Xunit2" /> <PackageReference Include="Akka.TestKit.Xunit2" />
<PackageReference Include="coverlet.collector" /> <PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NSubstitute" /> <PackageReference Include="NSubstitute" />
<PackageReference Include="xunit" /> <PackageReference Include="xunit" />

View File

@@ -0,0 +1,113 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NSubstitute;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.NotificationOutbox.Delivery;
using ScadaLink.NotificationService;
namespace ScadaLink.NotificationOutbox.Tests;
/// <summary>
/// Task 17: Tests for <see cref="ServiceCollectionExtensions.AddNotificationOutbox"/> — the
/// DI registration extension for the Notification Outbox component. The extension binds
/// <see cref="NotificationOutboxOptions"/> from the <c>ScadaLink:NotificationOutbox</c>
/// configuration section and registers the channel delivery adapter(s).
///
/// The Host wires both <c>AddNotificationService</c> and <c>AddNotificationOutbox</c> on the
/// central node; these tests do the same so the
/// adapter's SMTP dependencies (<c>Func&lt;ISmtpClientWrapper&gt;</c>, <c>OAuth2TokenService</c>,
/// <c>NotificationOptions</c>) are satisfied. <see cref="INotificationRepository"/> — which the
/// <see cref="EmailNotificationDeliveryAdapter"/> takes directly and is registered scoped by the
/// Configuration Database component — is supplied here by a lightweight stub.
/// </summary>
public class ServiceRegistrationTests
{
private static ServiceProvider BuildProvider()
{
var services = new ServiceCollection();
// Empty configuration: the options binding must still succeed and yield the
// documented NotificationOutboxOptions defaults.
var configuration = new ConfigurationBuilder().Build();
services.AddSingleton<IConfiguration>(configuration);
services.AddLogging();
// INotificationRepository is registered scoped by the Configuration Database
// component in production; a no-op stub stands in for it here.
services.AddScoped<INotificationRepository>(_ => Substitute.For<INotificationRepository>());
services.AddNotificationService();
services.AddNotificationOutbox();
return services.BuildServiceProvider();
}
[Fact]
public void AddNotificationOutbox_RegistersNotificationOutboxOptions_WithDefaults()
{
using var provider = BuildProvider();
var options = provider.GetRequiredService<IOptions<NotificationOutboxOptions>>().Value;
Assert.NotNull(options);
Assert.Equal(TimeSpan.FromSeconds(10), options.DispatchInterval);
Assert.Equal(100, options.DispatchBatchSize);
}
[Fact]
public void AddNotificationOutbox_OptionsSection_IsTheNotificationOutboxConfigPath()
{
Assert.Equal(
"ScadaLink:NotificationOutbox",
NotificationOutbox.ServiceCollectionExtensions.OptionsSection);
}
[Fact]
public void AddNotificationOutbox_BindsNotificationOutboxOptions_FromConfiguration()
{
var services = new ServiceCollection();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["ScadaLink:NotificationOutbox:DispatchBatchSize"] = "250",
})
.Build();
services.AddSingleton<IConfiguration>(configuration);
services.AddLogging();
services.AddScoped<INotificationRepository>(_ => Substitute.For<INotificationRepository>());
services.AddNotificationService();
services.AddNotificationOutbox();
using var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<NotificationOutboxOptions>>().Value;
Assert.Equal(250, options.DispatchBatchSize);
}
[Fact]
public void AddNotificationOutbox_RegistersEmailDeliveryAdapter()
{
using var provider = BuildProvider();
using var scope = provider.CreateScope();
var adapter = scope.ServiceProvider.GetRequiredService<EmailNotificationDeliveryAdapter>();
Assert.NotNull(adapter);
Assert.Equal(NotificationType.Email, adapter.Type);
}
[Fact]
public void AddNotificationOutbox_RegistersEmailAdapter_AsINotificationDeliveryAdapter()
{
using var provider = BuildProvider();
using var scope = provider.CreateScope();
var adapters = scope.ServiceProvider.GetServices<INotificationDeliveryAdapter>().ToList();
var email = Assert.Single(adapters);
Assert.IsType<EmailNotificationDeliveryAdapter>(email);
Assert.Equal(NotificationType.Email, email.Type);
}
}