I'm currently working on a C# web application and I'm trying to get push notifications to work using the PushSharp package. I have all of my code for pushing notifications in the Global.asax file in my project, but I keep getting the error:
The collection has been marked as complete with regards to additions.
Here is my Global.asax file:
using BYC.Models;
using BYC.Models.Enums;
using Newtonsoft.Json.Linq;
using PushSharp.Apple;
using PushSharp.Google;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
namespace BYC
{
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
protected void Application_End()
{
PushBrokerSingleton pbs = new PushBrokerSingleton();
pbs.SendQueuedNotifications();
}
}
public sealed class PushBrokerSingleton
{
private static ApnsServiceBroker Apns { get; set; }
private static GcmServiceBroker Gcm { get; set; }
private static bool ApnsStarted = false;
private static bool GcmStarted = false;
private static object AppleSyncVar = new object();
private static object GcmSyncVar = new object();
private static readonly log4net.ILog log = log4net.LogManager.GetLogger
(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public PushBrokerSingleton()
{
if (Apns == null)
{
string thumbprint = (AppSettings.Instance["APNS:Thumbprint"]);
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
ApnsConfiguration.ApnsServerEnvironment production = Convert.ToBoolean(AppSettings.Instance["APNS:Production"]) ?
ApnsConfiguration.ApnsServerEnvironment.Production : ApnsConfiguration.ApnsServerEnvironment.Sandbox;
X509Certificate2 appleCert = store.Certificates
.Cast<X509Certificate2>()
.SingleOrDefault(c => string.Equals(c.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase));
ApnsConfiguration apnsConfig = new ApnsConfiguration(production, appleCert);
Apns = new ApnsServiceBroker(apnsConfig);
Apns.OnNotificationFailed += (notification, aggregateEx) => {
aggregateEx.Handle(ex => {
// See what kind of exception it was to further diagnose
if (ex is ApnsNotificationException)
{
var notificationException = ex as ApnsNotificationException;
// Deal with the failed notification
var apnsNotification = notificationException.Notification;
var statusCode = notificationException.ErrorStatusCode;
log.Error($"Notification Failed: ID={apnsNotification.Identifier}, Code={statusCode}");
}
else {
// Inner exception might hold more useful information like an ApnsConnectionException
log.Error($"Notification Failed for some (Unknown Reason) : {ex.InnerException}");
}
// Mark it as handled
return true;
});
};
Apns.OnNotificationSucceeded += (notification) => {
log.Info("Notification Successfully Sent to: " + notification.DeviceToken);
};
}
if(Gcm == null)
{
GcmConfiguration gcmConfig = new GcmConfiguration(AppSettings.Instance["GCM:Token"]);
Gcm = new GcmServiceBroker(gcmConfig);
}
}
public bool QueueNotification(Notification notification, Device device)
{
if (!ApnsStarted)
{
ApnsStarted = true;
lock (AppleSyncVar)
{
Apns.Start();
}
}
if(!GcmStarted)
{
GcmStarted = true;
lock (GcmSyncVar)
{
Gcm.Start();
}
}
switch (device.PlatformType)
{
case PlatformType.iOS:
return QueueApplePushNotification(notification, device.PushRegistrationToken);
case PlatformType.Android:
return QueueAndroidPushNotification(notification, device.PushRegistrationToken);
default: return false;
}
}
private bool QueueApplePushNotification(Notification notification, string pushNotificationToken)
{
string appleJsonFormat = "{\"aps\": {\"alert\":" + '"' + notification.Subject + '"' + ",\"sound\": \"default\", \"badge\": " + notification.BadgeNumber + "}}";
lock (AppleSyncVar)
{
Apns.QueueNotification(new ApnsNotification()
{
DeviceToken = pushNotificationToken,
Payload = JObject.Parse(appleJsonFormat)
});
}
return true;
}
private bool QueueAndroidPushNotification(Notification notification, string pushNotificationToken)
{
string message = "{\"alert\":\"" + notification.Subject + "\",\"badge\":" + notification.BadgeNumber + "\"}";
lock (GcmSyncVar)
{
Gcm.QueueNotification(new GcmNotification()
{
RegistrationIds = new List<string>
{
pushNotificationToken
},
Data = JObject.Parse(message),
Notification = JObject.Parse(message)
});
}
return true;
}
public void SendQueuedNotifications()
{
if(Apns != null)
{
if (ApnsStarted)
{
lock(AppleSyncVar){
Apns.Stop();
log.Info("Sent Apns Notifications");
ApnsStarted = false;
}
}
}
if(Gcm != null)
{
if (GcmStarted)
{
lock (GcmSyncVar)
{
Gcm.Stop();
log.Info("Sent Gcm Notifications");
GcmStarted = false;
}
}
}
}
}
}
That happens when you try and reuse an instance of a service broker (eg: ApnsServiceBroker
) which Stop()
has been called on.
I'm guessing your Application_End
is getting called at some point and Application_Start
gets called again, but since PushBrokerSingleton.Apns
is not null (it's a static field so it must live on even though the Application has stopped/started), it never gets recreated.
PushSharp is a hard thing to make work nicely with the ASP.NET pattern, some sort of service daemon would be better.
The main issue is that your app may be recycled or ended when you don't expect it to. Unrelated requests in the same app can take down your process, or your AppDomain may be torn down. If this happens and the brokers' Stop()
calls can't end successfully, some queued messages could be lost. Here's a great article on some of the caveats: http://haacked.com/archive/2011/10/16/the-dangers-of-implementing-recurring-background-tasks-in-asp-net.aspx/ In practice, this may not be a big deal, and you can certainly mitigate parts of it, but keep it in mind.
Having said all that, I think a simple fix would be to create a new instance of PushBrokerSingleton.Apns
and PushBrokerSingleton.Gcm
in your Application_Start
. This may cause other issues for you so I'm not sure if it's the right fix, but it will work around the issue that the broker is not meant to be reused after Stop()
has been called.
I'm also going to consider adding some way to 'reset' the collection. I'm not sure if doing this automatically after .Stop()
ends is a good idea, but I may look at adding a .Reset()
or similar kind of method to achieve this. In any case, creating a new broker instance is perfectly acceptable for now.