\$\begingroup\$
\$\endgroup\$
I have a Pub/Sub class that will call Notify()
when published and that Notify()
could take around 10 seconds, so I need to make sure that this does not block the UI.
public static class NotificationService
{
private static Dictionary<string, INotification> Observers { get; }
static NotificationService()
{
Observers = new Dictionary<string, INotification>();
}
public static void Publish(string title, string requester)
{
foreach (var observer in Observers)
{
var notification = observer.Value;
new Thread(() =>
{
Thread.CurrentThread.IsBackground = true;
notification.Notify(title, requester);
}).Start();
}
}
public static void Subscribe(INotification notification)
{
INotification notificationValue;
if (Observers.TryGetValue(notification.NotificationName, out notificationValue))
{
// Observer already exists
return;
}
Observers[notification.NotificationName] = notification;
}
public static void UnSubscribe(INotification notification)
{
INotification notificationValue;
if (!Observers.TryGetValue(notification.NotificationName, out notificationValue))
{
// Observer doesn't exists
return;
}
Observers.Remove(notification.NotificationName);
}
}
INotification
public interface INotification
{
string NotificationName { get; }
bool Notify(string title, string requester);
}
Example implementation of INotification
public class EmailMessageNotification : INotification
{
public EmailMessageNotification(EmailNotificationSettingsService settings)
{
EmailNotificationSettings = settings;
}
private EmailNotificationSettingsService EmailNotificationSettings { get; }
public string NotificationName => "EmailMessageNotification";
public bool Notify(string title, string requester)
{
var configuration = GetConfiguration();
if (!ValidateConfiguration(configuration))
{
return false;
}
var message = new MailMessage
{
IsBodyHtml = true,
To = { new MailAddress(configuration.RecipientEmail) },
Body = $"User {requester} has requested {title}!",
From = new MailAddress(configuration.EmailUsername),
Subject = $"New Request for {title}!"
};
try
{
using (var smtp = new SmtpClient(configuration.EmailHost, configuration.EmailPort))
{
smtp.Credentials = new NetworkCredential(configuration.EmailUsername, configuration.EmailPassword);
smtp.EnableSsl = configuration.Ssl;
smtp.Send(message);
return true;
}
}
catch (SmtpException smtp)
{
Log.Fatal(smtp);
}
catch (Exception e)
{
Log.Fatal(e);
}
return false;
}
private EmailNotificationSettings GetConfiguration()
{
// Gets and Returns Config
}
private bool ValidateConfiguration(EmailNotificationSettings settings)
{
// Do some validation
}
Can anyone see anything wrong with this?
BCdotWEB
11.4k2 gold badges28 silver badges45 bronze badges
asked Mar 17, 2016 at 12:00
1 Answer 1
\$\begingroup\$
\$\endgroup\$
4
- It seems that you have a global static logger methods. It is not a good practice, as you're loosing the possibility to tune logging levels for different areas of your application (e.g. setting log level for
EmailMessageNotification
toDEBUG
while keepingINFO
level everywhere else. I recommend to follow the logging patterns suggested for your logging framework (see Creating loggers for NLog for example). - The code is not thread-safe, so you may get issues if your application publishes, subscribes or unsubscribes in different threads. Use either locking around
Observers
collection or ConcurrentDictionary
. Fatal
logging level should be used only when the application encounters a critical error after which it usually cannot continue to run. In your case I would suggest to useError
log level when email cannot be sent.NotificationService
is static, which means that you will have a strong dependencies in your code on this class. I suggest to introduce IoC framework (check out Autofac or StructureMap).- The response of
INotification.Notify(string, string)
is not used, so it can bevoid
. - Your current code requires spinning up threads per each observer per each notification. I suggest to switch to asynchronous processing model, changing the return type of
INotification.Notify(string, string)
toTask
. - Most likely you would want to wrap the notification of each listener in
try..catch
to avoid side effects when buggyINotification
subscribes to your service
As a result:
INotification
:
public interface INotification
{
string NotificationName { get; }
Task NotifyAsync(string title, string requester);
}
EmailMessageNotification
:
public class EmailMessageNotification : INotification
{
private static Logger logger = LogManager.GetCurrentClassLogger();
public EmailMessageNotification(EmailNotificationSettingsService settings)
{
EmailNotificationSettings = settings;
}
private EmailNotificationSettingsService EmailNotificationSettings { get; }
public string NotificationName => "EmailMessageNotification";
public async Task NotifyAsync(string title, string requester)
{
var configuration = GetConfiguration();
if (!ValidateConfiguration(configuration))
{
return;
}
var message = new MailMessage
{
IsBodyHtml = true,
To = { new MailAddress(configuration.RecipientEmail) },
Body = $"User {requester} has requested {title}!",
From = new MailAddress(configuration.EmailUsername),
Subject = $"New Request for {title}!"
};
try
{
using (var smtp = new SmtpClient(configuration.EmailHost, configuration.EmailPort))
{
smtp.Credentials = new NetworkCredential(configuration.EmailUsername, configuration.EmailPassword);
smtp.EnableSsl = configuration.Ssl;
await smtp.SendMailAsync(message).ConfigureAwait(false);
}
}
catch (SmtpException smtp)
{
logger.Error(smtp);
}
catch (Exception e)
{
logger.Error(e);
}
}
private EmailNotificationSettings GetConfiguration()
{
throw new NotImplementedException();
}
private bool ValidateConfiguration(EmailNotificationSettings settings)
{
throw new NotImplementedException();
}
}
NotificationService
:
public interface INotificationService
{
void Publish(string title, string requester);
void Subscribe(INotification notification);
void UnSubscribe(INotification notification);
}
public class NotificationService : INotificationService
{
private static Logger logger = LogManager.GetCurrentClassLogger();
private ConcurrentDictionary<string, INotification> Observers { get; } = new ConcurrentDictionary<string, INotification>();
private static async Task NotifyAsync(INotification notification, string title, string requester)
{
try
{
await notification.NotifyAsync(title, requester).ConfigureAwait(false);
}
catch (Exception ex)
{
logger.Error(ex, $"Notification '{notification.NotificationName}' failed with exception");
}
}
public async void Publish(string title, string requester)
{
IEnumerable<Task> notificationTasks = Observers.Values.Select(notification => NotifyAsync(notification, title, requester));
await Task.WhenAll(notificationTasks).ConfigureAwait(false);
}
public void Subscribe(INotification notification)
{
Observers.TryAdd(notification.NotificationName, notification);
}
public void UnSubscribe(INotification notification)
{
Observers.TryRemove(notification.NotificationName, out notification);
}
}
answered Mar 23, 2016 at 14:45
-
\$\begingroup\$ Regarding the
NotificationService
being static, this is because I am subscribing observers from different places in the Application. So If I wanted to Pusblish to all observers I would need tonew()
and then resubscribe all observers? Regarding the loggers, Looks like I removed them from the original question (I trimmed some usless stuff out). They are private static per class and not something global. \$\endgroup\$Jamie Rees– Jamie Rees2016年03月23日 14:53:12 +00:00Commented Mar 23, 2016 at 14:53 -
\$\begingroup\$ IoC framework will allow you to use
NotificationService
as a dependency (receive the instance ofINotificationService
in the constructor) rather than instantiate it yourselves. I would recommend you to look deeper into Dependency Injection pattern \$\endgroup\$almaz– almaz2016年03月24日 09:52:32 +00:00Commented Mar 24, 2016 at 9:52 -
\$\begingroup\$ I am currently using an IOC container with constructor injection, so get the instance of the service (Like a singleton) via DI? \$\endgroup\$Jamie Rees– Jamie Rees2016年03月24日 10:02:59 +00:00Commented Mar 24, 2016 at 10:02
-
1\$\begingroup\$ yes, wherever you need an instance of
INotificationService
- just inject it in constructor. At the registration time you should specify that only one instance of theNotificationService
should be created (SingleInstance
in Autofac,.Singleton()
in StructureMap) \$\endgroup\$almaz– almaz2016年03月25日 13:29:59 +00:00Commented Mar 25, 2016 at 13:29
Explore related questions
See similar questions with these tags.
lang-cs