Thursday, May 15, 2025
0 comments

Ultimate Guide to Sending Rich Push Notifications to Multiple Android Apps with ASP.NET Core, OneSignal, and Razor UI

11:10 AM

 



Welcome to this exhaustive guide on building a powerful push notification system for Android apps using ASP.NET Core, OneSignal, and a Razor-based user interface. If you’re a developer aiming to send rich notifications with Bangla and English text, images, and icons to multiple Android apps, this blog post is your one-stop resource. We’ll cover everything from setting up the backend to designing an intuitive UI, with complete C# code examples and a focus on real-world applications like e-commerce.


Table of Contents
  1. Introduction
    • What Are Rich Push Notifications?
    • Why ASP.NET Core, OneSignal, and Razor?
    • Goals of This Guide
  2. Business Case
    • Scenario: Multi-App E-Commerce Ecosystem
    • Objectives and Requirements
  3. Technical Overview
    • Android Push Notification Architecture
    • OneSignal’s Role in Android Notifications
    • Handling Bangla and English Text
    • Role of Razor UI
  4. Prerequisites
    • Tools and Accounts Needed
    • Android App Setup
  5. Step-by-Step Implementation
    • Setting Up the ASP.NET Core MVC Project
    • Configuring OneSignal for Multiple Android Apps
    • Designing Notification Models
    • Building the Notification Service
    • Creating the API Controller
    • Designing the Razor UI
    • Implementing Error Handling
    • Adding Logging and Monitoring
    • Testing the Solution
  6. Complete Code Example
    • Project Structure
    • Configuration Files
    • Models
    • Services
    • Controllers
    • Razor Views
    • Program Setup
  7. Advanced Scenarios
    • Localization for Bangla and English
    • Dynamic Content Personalization
    • Handling Images and Icons
    • High-Volume Notification Campaigns
    • Rate Limiting and Throttling
    • A/B Testing
  8. Error Handling Mechanisms
    • Handling API Failures
    • Managing Rate Limits
    • Dealing with Invalid Devices
    • Retry Policies with Polly
    • UI Error Feedback
  9. Pros and Cons
    • Advantages of OneSignal with ASP.NET Core and Razor
    • Limitations and Challenges
  10. Alternatives to OneSignal
    • Firebase Cloud Messaging (FCM)
    • Amazon SNS
    • Custom Push Server
  11. Comparison with OneSignal
    • Feature Comparison
    • Pricing Comparison
    • Scalability and Performance
  12. Best Practices
    • Security Considerations
    • Performance Optimization
    • Localization and Unicode Support
    • User Consent and Compliance
    • UI Usability
  13. Conclusion
  14. References

1. Introduction
What Are Rich Push Notifications?
Rich push notifications are advanced notifications for Android devices, designed to boost engagement through multimedia and interactivity. They include:
  • Multilingual Text: Support for Bangla (Bengali) and English.
  • Images: Large images (e.g., 1440x720px, 2:1 ratio) for visual appeal.
  • Icons: Small and large icons for branding.
  • Action Buttons: Interactive options like “Shop Now.”
  • Custom Data: Metadata for deep linking or analytics.
Delivered via Firebase Cloud Messaging (FCM), these notifications appear in the Android notification shade, often expanding to show images or buttons.
Why ASP.NET Core, OneSignal, and Razor?
ASP.NET Core is a robust, cross-platform framework for building scalable web applications. Its benefits include:
  • Scalability: Handles high traffic and large user bases.
  • Dependency Injection: Simplifies integration with APIs.
  • MVC Pattern: Supports Razor for dynamic UIs.
  • Security: Built-in HTTPS and authentication.
OneSignal is a leading push notification platform, ideal for Android due to:
  • Ease of Use: Simple REST API and Android SDK.
  • Rich Notifications: Supports images, icons, and actions.
  • Localization: Handles Unicode for Bangla.
  • Multi-App Support: Manages multiple apps with one API.
  • Free Tier: Unlimited mobile push sends.
Razor UI enhances the solution by:
  • User Accessibility: Allows non-technical users (e.g., marketing teams) to send notifications.
  • Dynamic Forms: Simplifies input for text, images, and scheduling.
  • Real-Time Feedback: Displays success or error messages.
Goals of This Guide
This guide aims to:
  • Provide a complete C# implementation for sending rich notifications to multiple Android apps.
  • Support Bangla and English text, images, and icons.
  • Include a Razor-based UI for managing notifications.
  • Implement robust error handling for API failures, rate limits, and invalid devices.
  • Address advanced scenarios like localization, personalization, and high-volume campaigns.
  • Offer best practices for security, performance, compliance, and UI usability.
  • Compare OneSignal with alternatives like FCM.
  • Serve as a publishable blog post for developers and stakeholders.

2. Business Case
Scenario: Multi-App E-Commerce Ecosystem
Consider “BazaarHub,” a Bangladeshi e-commerce company with three Android apps:
  • BazaarHub Fashion: Clothing and accessories.
  • BazaarHub Electronics: Gadgets and appliances.
  • BazaarHub Grocery: Grocery delivery.
These apps share users but serve distinct purposes. BazaarHub wants to send unified rich push notifications for cross-app campaigns, like a Ramadan sale, with Bangla and English text to reach diverse users in Bangladesh and beyond.
Objectives
  1. Unified Campaigns: Send identical notifications to all apps.
  2. Localized Content: Include Bangla (“৫০% ছাড়!”) and English (“50% Off!”).
  3. Visual Engagement: Use product images and branded icons.
  4. User Interaction: Add action buttons for deep linking.
  5. User-Friendly UI: Enable marketing teams to send notifications via a web interface.
  6. Reliability: Ensure delivery during high-traffic campaigns.
  7. Compliance: Adhere to user consent and privacy laws.
Requirements
  • Rich Notifications: Bangla/English text, images, and app-specific icons.
  • Multi-App Targeting: Send to all apps via one API call or UI form.
  • Scheduling: Deliver at optimal times (e.g., 8 PM local time).
  • Error Handling: Manage API errors, rate limits, and undeliverable devices.
  • Razor UI: Intuitive interface for composing and sending notifications.
  • Analytics: Track delivery, open rates, and clicks.
  • Scalability: Support 100,000+ users.
Example Scenarios
  1. Ramadan Sale: All apps send a notification with a sale banner, Bangla/English text, and a “Shop Now” button.
  2. Cart Abandonment: Remind users of abandoned items with product images and localized text.
  3. New Product Launch: Announce a smartphone in the Electronics app, with English fallback.

3. Technical Overview
Android Push Notification Architecture
Android notifications use Firebase Cloud Messaging (FCM):
  • Application Server: ASP.NET Core app sends requests to OneSignal’s API.
  • OneSignal: Manages subscriptions, formats notifications, and routes to FCM.
  • FCM: Delivers notifications to Android devices.
  • Android App: Renders notifications with text, images, and icons.
OneSignal simplifies FCM integration, providing a unified API for rich notifications.
OneSignal’s Role in Android Notifications
OneSignal’s Android SDK integrates with FCM to:
  • Register devices for push notifications.
  • Handle rich media (images up to 5MB, icons).
  • Support Unicode for Bangla (U+0980 to U+09FF).
  • Enable action buttons and deep links.
  • Provide analytics for tracking.
OneSignal supports multiple apps via multiple App IDs or a single account.
Handling Bangla and English Text
  • Unicode Support: Bangla uses UTF-8, supported by OneSignal and Android.
  • Localization: Use contents field for both languages, with device language detection.
  • Fallback: Default to English for non-Bangla devices.
Role of Razor UI
The Razor UI will:
  • Provide a form to input Bangla/English text, image URLs, and scheduling details.
  • Allow selection of target apps and segments.
  • Display success/error messages after sending.
  • Use Bootstrap for responsive design.
  • Integrate with the backend API for seamless operation.

4. Prerequisites
Tools and Accounts Needed
  • Development Environment:
    • Visual Studio 2022 or VS Code with C# extensions.
    • Android Studio for SDK testing.
  • OneSignal Account:
    • Sign up at onesignal.com.
    • Create apps for Fashion, Electronics, and Grocery.
    • Obtain App IDs and REST API Key.
  • Firebase Account:
  • NuGet Packages:
    • Microsoft.AspNetCore.Mvc.Razor
    • Microsoft.Extensions.Http
    • System.Text.Json
    • Microsoft.Extensions.Logging
    • Polly
    • Serilog.AspNetCore
Android App Setup
For each app:
  1. Add OneSignal SDK: In app/build.gradle:
    gradle
    dependencies {
        implementation 'com.onesignal:OneSignal:[5.0.0, 5.99.99]'
    }
  2. Initialize OneSignal: In MainActivity.java:
    java
    import com.onesignal.OneSignal;
    import com.onesignal.OSNotificationOpenResult;
    import com.onesignal.OneSignalNotificationOpenedHandler;
    
    public class MainActivity extends AppCompatActivity {
        private static final String ONESIGNAL_APP_ID = "your-app-id";
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            OneSignal.initWithContext(this, ONESIGNAL_APP_ID);
            OneSignal.setNotificationOpenedHandler(new NotificationOpenedHandler());
            OneSignal.setLanguage("bn");
            OneSignal.promptForPushNotifications();
        }
    }
    
    class NotificationOpenedHandler implements OneSignalNotificationOpenedHandler {
        @Override
        public void notificationOpened(OSNotificationOpenResult result) {
            String url = result.getNotification().getAdditionalData().optString("url");
            if (url != null) {
                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                startActivity(intent);
            }
        }
    }
  3. Configure FCM:
    • Link to Firebase project.
    • Add google-services.json to app directory.
  4. Set Icons: In AndroidManifest.xml:
    xml
    <application>
        <meta-data
            android:name="com.onesignal.NotificationLargeIcon"
            android:resource="@drawable/large_icon" />
        <meta-data
            android:name="com.onesignal.NotificationSmallIcon" 
            android:resource="@drawable/small_icon" />
    </application>
    Place large_icon.png (96x96px) and small_icon.png (24x24px) in res/drawable.
  5. Test Subscription:
    • Run the app and verify subscription in OneSignal’s dashboard.

5. Step-by-Step Implementation
Setting Up the ASP.NET Core MVC Project
  1. Create an MVC Project:
    bash
    dotnet new mvc -n BazaarHubNotifications
    cd BazaarHubNotifications
  2. Install NuGet Packages:
    bash
    dotnet add package Microsoft.Extensions.Http
    dotnet add package System.Text.Json
    dotnet add package Microsoft.Extensions.Logging
    dotnet add package Polly --version 8.0.0
    dotnet add package Serilog.AspNetCore --version 8.0.0
  3. Project Structure:
    plaintext
    BazaarHubNotifications/
    ├── Configuration/
    │   ├── OneSignalSettings.cs
    ├── Models/
    │   ├── NotificationRequest.cs
    │   ├── OneSignalNotification.cs
    ├── Services/
    │   ├── INotificationService.cs
    │   ├── OneSignalNotificationService.cs
    ├── Controllers/
    │   ├── NotificationsController.cs
    │   ├── HomeController.cs
    ├── Views/
    │   ├── Home/
    │   │   ├── Index.cshtml
    │   ├── Notifications/
    │   │   ├── Send.cshtml
    │   │   ├── Result.cshtml
    │   ├── Shared/
    │   │   ├── _Layout.cshtml
    ├── wwwroot/
    │   ├── css/
    │   │   ├── site.css
    │   ├── js/
    │   │   ├── site.js
    │   ├── lib/
    │   │   ├── bootstrap/
    ├── Logs/
    │   ├── notification.log
    ├── appsettings.json
    ├── Program.cs
    ├── BazaarHubNotifications.csproj
  4. Configure Serilog: In Program.cs:
    csharp
    using Microsoft.AspNetCore.Mvc;
    using Serilog;
    using Serilog.Events;
    using BazaarHubNotifications.Configuration;
    using BazaarHubNotifications.Services;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Configure Serilog
    Log.Logger = new LoggerConfiguration()
        .MinimumLevel.Information()
        .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
        .WriteTo.Console()
        .WriteTo.File("Logs/notification.log", rollingInterval: RollingInterval.Day)
        .CreateLogger();
    
    builder.Host.UseSerilog();
    
    // Add services
    builder.Services.AddControllersWithViews();
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    builder.Services.Configure<OneSignalSettings>(builder.Configuration.GetSection("OneSignal"));
    builder.Services.AddHttpClient<INotificationService, OneSignalNotificationService>();
    
    var app = builder.Build();
    
    // Configure pipeline
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseRouting();
    app.UseAuthorization();
    app.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
    
    try
    {
        Log.Information("Starting application");
        app.Run();
    }
    catch (Exception ex)
    {
        Log.Fatal(ex, "Application failed to start");
        throw;
    }
    finally
    {
        Log.CloseAndFlush();
    }
Configuring OneSignal for Multiple Android Apps
  1. Create OneSignal Apps:
    • Create apps for Fashion, Electronics, and Grocery in the OneSignal dashboard.
    • Add FCM Server Key and Sender ID from Firebase.
    • Enable large image support.
  2. Store Credentials: In appsettings.json:
    json
    {
      "OneSignal": {
        "Apps": [
          { "AppId": "fashion-app-id", "Name": "BazaarHub Fashion" },
          { "AppId": "electronics-app-id", "Name": "BazaarHub Electronics" },
          { "AppId": "grocery-app-id", "Name": "BazaarHub Grocery" }
        ],
        "ApiKey": "your-rest-api-key"
      },
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*"
    }
    Create Configuration/OneSignalSettings.cs:
    csharp
    namespace BazaarHubNotifications.Configuration;
    
    public class OneSignalSettings
    {
        public List<OneSignalApp> Apps { get; set; } = new();
        public string ApiKey { get; set; }
    }
    
    public class OneSignalApp
    {
        public string AppId { get; set; }
        public string Name { get; set; }
    }
Designing Notification Models
  1. Input Model: Create Models/NotificationRequest.cs:
    csharp
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    
    namespace BazaarHubNotifications.Models;
    
    public class NotificationRequest
    {
        [Required(ErrorMessage = "Bangla title is required")]
        public string TitleBangla { get; set; }
    
        public string TitleEnglish { get; set; }
    
        [Required(ErrorMessage = "Bangla message is required")]
        public string MessageBangla { get; set; }
    
        public string MessageEnglish { get; set; }
    
        [Url(ErrorMessage = "Invalid image URL")]
        public string ImageUrl { get; set; }
    
        [Url(ErrorMessage = "Invalid large icon URL")]
        public string LargeIconUrl { get; set; }
    
        [Url(ErrorMessage = "Invalid small icon URL")]
        public string SmallIconUrl { get; set; }
    
        public List<string> AppIds { get; set; } = new();
    
        public List<string> UserIds { get; set; } = new();
    
        public List<string> Segments { get; set; } = new();
    
        public DateTime? ScheduleTime { get; set; }
    
        public List<NotificationAction> Actions { get; set; } = new();
    
        public Dictionary<string, string> CustomData { get; set; } = new();
    }
    
    public class NotificationAction
    {
        public string Id { get; set; }
        public string Text { get; set; }
        public string Url { get; set; }
    }
  2. OneSignal Payload Model: Create Models/OneSignalNotification.cs:
    csharp
    using System.Collections.Generic;
    using System.Text.Json.Serialization;
    
    namespace BazaarHubNotifications.Models;
    
    public class OneSignalNotification
    {
        [JsonPropertyName("app_id")]
        public string AppId { get; set; }
    
        [JsonPropertyName("contents")]
        public Dictionary<string, string> Contents { get; set; } = new();
    
        [JsonPropertyName("headings")]
        public Dictionary<string, string> Headings { get; set; } = new();
    
        [JsonPropertyName("included_segments")]
        public List<string> IncludedSegments { get; set; } = new();
    
        [JsonPropertyName("include_external_user_ids")]
        public List<string> IncludeExternalUserIds { get; set; } = new();
    
        [JsonPropertyName("big_picture")]
        public string BigPicture { get; set; }
    
        [JsonPropertyName("large_icon")]
        public string LargeIcon { get; set; }
    
        [JsonPropertyName("small_icon")]
        public string SmallIcon { get; set; }
    
        [JsonPropertyName("actions")]
        public List<NotificationAction> Actions { get; set; } = new();
    
        [JsonPropertyName("send_after")]
        public string SendAfter { get; set; }
    
        [JsonPropertyName("data")]
        public Dictionary<string, string> Data { get; set; } = new();
    
        [JsonPropertyName("idempotency_key")]
        public string IdempotencyKey { get; set; }
    }
Building the Notification Service
  1. Define Interface: Create Services/INotificationService.cs:
    csharp
    using BazaarHubNotifications.Models;
    using System.Threading.Tasks;
    
    namespace BazaarHubNotifications.Services;
    
    public interface INotificationService
    {
        Task<(bool Success, string Message)> SendNotificationAsync(NotificationRequest request);
    }
  2. Implement Service: Create Services/OneSignalNotificationService.cs:
    csharp
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using BazaarHubNotifications.Configuration;
    using BazaarHubNotifications.Models;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net.Http;
    using System.Text;
    using System.Text.Json;
    using System.Threading.Tasks;
    using Polly;
    using Polly.Retry;
    
    namespace BazaarHubNotifications.Services;
    
    public class OneSignalNotificationService : INotificationService
    {
        private readonly HttpClient _httpClient;
        private readonly OneSignalSettings _settings;
        private readonly ILogger<OneSignalNotificationService> _logger;
        private readonly AsyncRetryPolicy _retryPolicy;
        private static readonly SemaphoreSlim _throttle = new SemaphoreSlim(10);
    
        public OneSignalNotificationService(
            HttpClient httpClient,
            IOptions<OneSignalSettings> settings,
            ILogger<OneSignalNotificationService> logger)
        {
            _httpClient = httpClient;
            _settings = settings.Value;
            _logger = logger;
            _httpClient.BaseAddress = new Uri("https://onesignal.com/api/v1/");
            _httpClient.DefaultRequestHeaders.Add("Authorization", $"Basic {_settings.ApiKey}");
    
            _retryPolicy = Policy
                .Handle<HttpRequestException>()
                .OrResult<HttpResponseMessage>(r => r.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
                .WaitAndRetryAsync(
                    retryCount: 3,
                    sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                    onRetryAsync: (result, timeSpan, retryAttempt, context) =>
                    {
                        _logger.LogWarning(
                            "Retry {RetryAttempt} after {TimeSpan}ms due to {Reason}",
                            retryAttempt, timeSpan.TotalMilliseconds,
                            result.Exception?.Message ?? result.Result.StatusCode.ToString());
                        return Task.CompletedTask;
                    });
        }
    
        public async Task<(bool Success, string Message)> SendNotificationAsync(NotificationRequest request)
        {
            await _throttle.WaitAsync();
            try
            {
                if (string.IsNullOrEmpty(request.TitleBangla) || string.IsNullOrEmpty(request.MessageBangla))
                {
                    _logger.LogError("Bangla title or message is missing");
                    return (false, "Bangla title and message are required.");
                }
    
                var targetAppIds = request.AppIds.Any()
                    ? _settings.Apps.Where(a => request.AppIds.Contains(a.AppId)).Select(a => a.AppId).ToList()
                    : _settings.Apps.Select(a => a.AppId).ToList();
    
                if (!targetAppIds.Any())
                {
                    _logger.LogError("No valid App IDs specified");
                    return (false, "No valid apps selected.");
                }
    
                var successes = new List<string>();
                var failures = new List<string>();
                var errorMessages = new List<string>();
    
                foreach (var appId in targetAppIds)
                {
                    var notification = new OneSignalNotification
                    {
                        AppId = appId,
                        Contents = new Dictionary<string, string>
                        {
                            { "bn", request.MessageBangla },
                            { "en", request.MessageEnglish ?? request.MessageBangla }
                        },
                        Headings = new Dictionary<string, string>
                        {
                            { "bn", request.TitleBangla },
                            { "en", request.TitleEnglish ?? request.TitleBangla }
                        },
                        IncludedSegments = request.Segments,
                        IncludeExternalUserIds = request.UserIds,
                        BigPicture = request.ImageUrl,
                        LargeIcon = request.LargeIconUrl,
                        SmallIcon = request.SmallIconUrl,
                        Actions = request.Actions,
                        SendAfter = request.ScheduleTime?.ToString("yyyy-MM-dd HH:mm:ss 'GMT'"),
                        Data = request.CustomData,
                        IdempotencyKey = Guid.NewGuid().ToString()
                    };
    
                    var json = JsonSerializer.Serialize(notification, new JsonSerializerOptions
                    {
                        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                        IgnoreNullValues = true
                    });
    
                    var content = new StringContent(json, Encoding.UTF8, "application/json");
    
                    var response = await _retryPolicy.ExecuteAsync(() =>
                        _httpClient.PostAsync("notifications", content));
    
                    if (response.IsSuccessStatusCode)
                    {
                        var responseContent = await response.Content.ReadAsStringAsync();
                        var result = JsonSerializer.Deserialize<Dictionary<string, object>>(responseContent);
                        if (result.ContainsKey("errors") && result["errors"].ToString().Contains("invalid_player_ids"))
                        {
                            var invalidIds = JsonSerializer.Deserialize<List<string>>(result["invalid_player_ids"].ToString());
                            _logger.LogWarning("Invalid player IDs for App ID {AppId}: {InvalidIds}", appId, string.Join(", ", invalidIds));
                            // Queue for cleanup
                        }
                        _logger.LogInformation(
                            "Notification sent successfully to App ID {AppId}. Notification ID: {NotificationId}",
                            appId, result["id"]);
                        successes.Add(_settings.Apps.First(a => a.AppId == appId).Name);
                    }
                    else
                    {
                        var errorContent = await response.Content.ReadAsStringAsync();
                        _logger.LogError(
                            "Failed to send notification to App ID {AppId}. Status: {StatusCode}, Error: {Error}",
                            appId, response.StatusCode, errorContent);
                        failures.Add(_settings.Apps.First(a => a.AppId == appId).Name);
                        errorMessages.Add($"Failed for {_settings.Apps.First(a => a.AppId == appId).Name}: {errorContent}");
                    }
                }
    
                if (failures.Any())
                {
                    _logger.LogWarning(
                        "Notification failed for {FailureCount} apps: {FailedApps}",
                        failures.Count, string.Join(", ", failures));
                    return (false, $"Failed to send to some apps: {string.Join("; ", errorMessages)}");
                }
    
                return (true, $"Notification sent successfully to: {string.Join(", ", successes)}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Unexpected error sending notification: {Message}", ex.Message);
                return (false, $"Unexpected error: {ex.Message}");
            }
            finally
            {
                _throttle.Release();
            }
        }
    }
Creating the API Controller
Create Controllers/NotificationsController.cs:
csharp
using Microsoft.AspNetCore.Mvc;
using BazaarHubNotifications.Models;
using BazaarHubNotifications.Services;
using System.Threading.Tasks;

namespace BazaarHubNotifications.Controllers;

[Route("api/[controller]")]
[ApiController]
public class NotificationsController : ControllerBase
{
    private readonly INotificationService _notificationService;

    public NotificationsController(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    [HttpPost("send")]
    public async Task<IActionResult> SendNotification([FromBody] NotificationRequest request)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var (success, message) = await _notificationService.SendNotificationAsync(request);
        if (success)
        {
            return Ok(new { Message = message });
        }

        return StatusCode(500, new { Message = message });
    }
}
Designing the Razor UI
Layout: Create Views/Shared/_Layout.cshtml:
cshtml
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>BazaarHub Notifications</title> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> </head> <body> <header> <nav class="navbar navbar-expand-sm navbar-dark bg-dark"> <div class="container"> <a class="navbar-brand" href="/">BazaarHub Notifications</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse"> <ul class="navbar-nav ms-auto"> <li class="nav-item"> <a class="nav-link" asp-controller="Notifications" asp-action="Send">Send Notification</a> </li> </ul> </div> </div> </nav> </header> <div class="container"> <main role="main" class="pb-3"> @RenderBody() </main> </div> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> @await RenderSectionAsync("Scripts", required: false) </body> </html>

 

Home Page: Update Views/Home/Index.cshtml:

cshtml

@{ ViewData["Title"] = "Home"; } <div class="text-center"> <h1>Welcome to BazaarHub Notifications</h1> <p>Use this system to send rich push notifications to your Android apps.</p> <a class="btn btn-primary" asp-controller="Notifications" asp-action="Send">Send a Notification</a> </div>


Send Notification Form: Create Views/Notifications/Send.cshtml:

cshtml
@model BazaarHubNotifications.Models.NotificationRequest @inject IConfiguration Configuration @using BazaarHubNotifications.Configuration @{ ViewData["Title"] = "Send Notification"; var oneSignalSettings = Configuration.GetSection("OneSignal").Get<OneSignalSettings>(); } <h1>Send Notification</h1> <form asp-action="Send" method="post" id="notificationForm"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="mb-3"> <label class="form-label">Bangla Title</label> <input asp-for="TitleBangla" class="form-control" required /> <span asp-validation-for="TitleBangla" class="text-danger"></span> </div> <div class="mb-3"> <label class="form-label">English Title</label> <input asp-for="TitleEnglish" class="form-control" /> <span asp-validation-for="TitleEnglish" class="text-danger"></span> </div> <div class="mb-3"> <label class="form-label">Bangla Message</label> <textarea asp-for="MessageBangla" class="form-control" required></textarea> <span asp-validation-for="MessageBangla" class="text-danger"></span> </div> <div class="mb-3"> <label class="form-label">English Message</label> <textarea asp-for="MessageEnglish" class="form-control"></textarea> <span asp-validation-for="MessageEnglish" class="text-danger"></span> </div> <div class="mb-3"> <label class="form-label">Image URL</label> <input asp-for="ImageUrl" class="form-control" type="url" /> <span asp-validation-for="ImageUrl" class="text-danger"></span> </div> <div class="mb-3"> <label class="form-label">Large Icon URL</label> <input asp-for="LargeIconUrl" class="form-control" type="url" /> <span asp-validation-for="LargeIconUrl" class="text-danger"></span> </div> <div class="mb-3"> <label class="form-label">Small Icon URL</label> <input asp-for="SmallIconUrl" class="form-control" type="url" /> <span asp-validation-for="SmallIconUrl" class="text-danger"></span> </div> <div class="mb-3"> <label class="form-label">Target Apps</label> @foreach (var app in oneSignalSettings.Apps) { <div class="form-check"> <input type="checkbox" class="form-check-input" name="AppIds" value="@app.AppId" id="app_@app.AppId" /> <label class="form-check-label" for="app_@app.AppId">@app.Name</label> </div> } </div> <div class="mb-3"> <label class="form-label">Segments</label> <input asp-for="Segments" class="form-control" placeholder="e.g., Active Users, VIP" /> <small class="form-text">Comma-separated segment names</small> </div> <div class="mb-3"> <label class="form-label">Schedule Time (UTC)</label> <input asp-for="ScheduleTime" class="form-control" type="datetime-local" /> </div> <div class="mb-3"> <label class="form-label">Action Button Text</label> <input name="Actions[0].Text" class="form-control" placeholder="e.g., Shop Now" /> </div> <div class="mb-3"> <label class="form-label">Action Button URL</label> <input name="Actions[0].Url" class="form-control" type="url" placeholder="e.g., https://bazaarhub.com/sale" /> </div> <button type="submit" class="btn btn-primary">Send Notification</button> </form> @section Scripts { <script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script> <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script> <script> $(document).ready(function () { $('#notificationForm').on('submit', function (e) { e.preventDefault(); var formData = $(this).serializeArray(); var data = {}; formData.forEach(function (item) { if (item.name.includes('[')) { var parts = item.name.split(/\[|\]/).filter(p => p); if (!data[parts[0]]) data[parts[0]] = []; data[parts[0]].push({ [parts[1]]: item.value }); } else if (item.name === 'AppIds' || item.name === 'Segments') { if (!data[item.name]) data[item.name] = []; data[item.name].push(item.value); } else { data[item.name] = item.value; } }); if (data.Segments) { data.Segments = data.Segments[0].split(',').map(s => s.trim()); } $.ajax({ url: '/api/notifications/send', type: 'POST', contentType: 'application/json', data: JSON.stringify(data), success: function (response) { window.location.href = '/Notifications/Result?success=true&message=' + encodeURIComponent(response.Message); }, error: function (xhr) { var errorMessage = xhr.responseJSON?.Message || 'An error occurred.'; window.location.href = '/Notifications/Result?success=false&message=' + encodeURIComponent(errorMessage); } }); }); }); </script> }


Result Page: Create Views/Notifications/Result.cshtml:

cs
@{ ViewData["Title"] = "Result"; var success = ViewBag.Success; var message = ViewBag.Message; } <div class="container mt-5"> <div class="text-center"> <h1 class="mb-4">Notification Result</h1> <div class="alert @(success ? "alert-success" : "alert-danger")" role="alert"> <strong>@(success ? "Success!" : "Error!")</strong> @message </div> <div class="mt-4"> <a class="btn btn-primary me-2" asp-action="Send"> <i class="bi bi-send"></i> Send Another Notification </a> <a class="btn btn-secondary" asp-controller="Home" asp-action="Index"> <i class="bi bi-house-door"></i> Back to Home </a> </div> </div> </div>

  1. Home Controller: Update Controllers/HomeController.cs:
    csharp
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Logging;
    using System.Diagnostics;
    
    namespace BazaarHubNotifications.Controllers;
    
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;
    
        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }
    
        public IActionResult Index()
        {
            return View();
        }
    
        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
    
    public class ErrorViewModel
    {
        public string RequestId { get; set; }
        public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
    }
  2. Notifications Controller (MVC): Create Controllers/NotificationsController.cs (merge with API controller):
    csharp
    using Microsoft.AspNetCore.Mvc;
    using BazaarHubNotifications.Models;
    using BazaarHubNotifications.Services;
    using System.Threading.Tasks;
    
    namespace BazaarHubNotifications.Controllers;
    
    public class NotificationsController : Controller
    {
        private readonly INotificationService _notificationService;
    
        public NotificationsController(INotificationService notificationService)
        {
            _notificationService = notificationService;
        }
    
        [HttpGet]
        public IActionResult Send()
        {
            return View(new NotificationRequest());
        }
    
        [HttpPost]
        public async Task<IActionResult> Send(NotificationRequest request)
        {
            if (!ModelState.IsValid)
            {
                return View(request);
            }
    
            var (success, message) = await _notificationService.SendNotificationAsync(request);
            ViewBag.Success = success;
            ViewBag.Message = message;
            return View("Result");
        }
    
        [HttpPost("api/notifications/send")]
        [ApiExplorerSettings(GroupName = "api")]
        public async Task<IActionResult> SendApi([FromBody] NotificationRequest request)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
    
            var (success, message) = await _notificationService.SendNotificationAsync(request);
            if (success)
            {
                return Ok(new { Message = message });
            }
    
            return StatusCode(500, new { Message = message });
        }
    }
  3. CSS Styling: Update wwwroot/css/site.css:
    css
    body {
        font-family: Arial, sans-serif;
    }
    
    .form-control {
        max-width: 500px;
    }
    
    .form-check {
        margin-bottom: 10px;
    }
    
    .alert {
        max-width: 600px;
        margin: 20px auto;
    }
    
    .btn-primary {
        margin-right: 10px;
    }
Implementing Error Handling
  1. Validation Errors:
    • Use DataAnnotations in NotificationRequest.
    • Display errors in the UI via asp-validation-for.
  2. API Failures:
    • Polly retries transient errors (e.g., HTTP 429).
    • Log detailed responses.
  3. Invalid Devices:
    • Handle invalid_player_ids in the service.
    • Log for cleanup.
  4. UI Feedback:
    • Show success/error messages on the Result page.
    • Use AJAX to prevent page reloads.
Adding Logging and Monitoring
  1. Serilog:
    • Logs to console and Logs/notification.log.
    • Capture API responses and errors.
  2. Monitoring:
    • Check OneSignal’s analytics for delivery and clicks.
    • Add a logs endpoint (optional):
      csharp
      [HttpGet("logs")]
      public IActionResult Logs()
      {
          var logs = System.IO.File.ReadAllLines("Logs/notification.log");
          return Json(logs);
      }

Program Setup
Program.cs
csharp
using Microsoft.AspNetCore.Mvc;
using Serilog;
using Serilog.Events;
using BazaarHubNotifications.Configuration;
using BazaarHubNotifications.Services;

var builder = WebApplication.CreateBuilder(args);

// Configure Serilog
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .WriteTo.Console()
    .WriteTo.File("Logs/notification.log", rollingInterval: RollingInterval.Day)
    .CreateLogger();

builder.Host.UseSerilog();

// Add services
builder.Services.AddControllersWithViews();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.Configure<OneSignalSettings>(builder.Configuration.GetSection("OneSignal"));
builder.Services.AddHttpClient<INotificationService, OneSignalNotificationService>();

var app = builder.Build();

// Configure pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

try
{
    Log.Information("Starting application");
    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application failed to start");
    throw;
}
finally
{
    Log.CloseAndFlush();
}

7. Advanced Scenarios
Localization for Bangla and English
  • Use contents and headings with "bn" and "en" keys.
  • Set device language in the Android app:
    java
    OneSignal.setLanguage("bn");
  • Fallback to English:
    csharp
    Contents = new Dictionary<string, string>
    {
        { "bn", request.MessageBangla },
        { "en", request.MessageEnglish ?? request.MessageBangla }
    };
Dynamic Content Personalization
  • Store user data as tags:
    java
    OneSignal.sendTag("username", "Rahim");
  • Include in CustomData:
    csharp
    request.CustomData = new Dictionary<string, string>
    {
        { "username", "Rahim" },
        { "product", "Smartphone" }
    };
  • Parse in the Android app:
    java
    String username = result.getNotification().getAdditionalData().optString("username");
Handling Images and Icons
  • Images: Use 1440x720px, host on a CDN, validate URLs:
    csharp
    if (!Uri.TryCreate(request.ImageUrl, UriKind.Absolute, out _))
    {
        return (false, "Invalid image URL.");
    }
  • Icons: Large (96x96px), small (24x24px), fallback to app resources.
High-Volume Notification Campaigns
  • Batch Processing:
    csharp
    public async Task<(bool, string)> SendToLargeAudienceAsync(NotificationRequest request, List<string> userIds)
    {
        const int batchSize = 2000;
        var successes = new List<string>();
        for (int i = 0; i < userIds.Count; i += batchSize)
        {
            request.UserIds = userIds.Skip(i).Take(batchSize).ToList();
            var (success, message) = await SendNotificationAsync(request);
            if (success) successes.Add(message);
        }
        return successes.Any() ? (true, string.Join("; ", successes)) : (false, "No batches succeeded.");
    }
Rate Limiting and Throttling
  • Use _throttle semaphore.
  • Polly handles HTTP 429 with exponential backoff.
A/B Testing
  • Create variants in OneSignal dashboard.
  • Assign via API:
    csharp
    notification.VariantName = "variant_a";

8. Error Handling Mechanisms
  • Validation: DataAnnotations and UI feedback.
  • API Failures: Polly retries, detailed logging.
  • Invalid Devices: Log and queue for cleanup.
  • UI Feedback: Success/error messages on Result page.
  • Rate Limits: Throttling and retries.

9. Pros and Cons
Advantages
  • OneSignal: Easy API, rich features, free tier.
  • ASP.NET Core: Scalable, secure, MVC support.
  • Razor UI: User-friendly, responsive.
Limitations
  • Dependency: Relies on OneSignal.
  • Image Restrictions: 5MB limit, no GIFs.
  • Cost: Advanced features require paid plans.

10. Alternatives to OneSignal
  • FCM: Free, but less targeting.
  • Amazon SNS: Scalable, complex setup.
  • Custom Server: Full control, high effort.

11. Comparison with OneSignal
Feature
OneSignal
FCM
SNS
Rich Notifications
Yes
Yes
Limited
Localization
Yes
Manual
Manual
Multi-App
Yes
Yes
Yes
Free Tier
Unlimited mobile push
Free
1M free

12. Best Practices
  • Security: Store API keys securely, use HTTPS.
  • Performance: Batch processing, caching.
  • Localization: UTF-8 for Bangla, fallback to English.
  • Compliance: Obtain user consent.
  • UI Usability: Responsive design, clear feedback.

13. Conclusion
This guide provides a complete solution for sending rich push notifications to multiple Android apps, with a Razor UI for ease of use. The implementation is scalable, localized, and robust, making it ideal for e-commerce and beyond.

14. References
 





0 comments:

 
Toggle Footer