<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anoush</title>
    <description>The latest articles on DEV Community by Anoush (@anoushnet).</description>
    <link>https://dev.to/anoushnet</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F191310%2F76a99ea2-cc58-47c7-8ae7-3dc10ae89178.jpg</url>
      <title>DEV Community: Anoush</title>
      <link>https://dev.to/anoushnet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anoushnet"/>
    <language>en</language>
    <item>
      <title>User Connectivity: Making the System Scale with Event Hub Partitions, ACA, and KEDA</title>
      <dc:creator>Anoush</dc:creator>
      <pubDate>Thu, 06 Aug 2026 15:57:15 +0000</pubDate>
      <link>https://dev.to/anoushnet/user-connectivity-making-the-system-scale-with-event-hub-partitions-aca-and-keda-3524</link>
      <guid>https://dev.to/anoushnet/user-connectivity-making-the-system-scale-with-event-hub-partitions-aca-and-keda-3524</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 of the User Connectivity Architecture series.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The first post in this series described the pattern: a heartbeat on a timer, an Event Hub, a worker writing sessions into Redis, and Redis key expiration driving facility online/offline status.&lt;/p&gt;

&lt;p&gt;One detail matters later. The heartbeat interval is not hard-coded in the client. The API tells the client when to call next, and the default is 30 seconds.&lt;/p&gt;

&lt;p&gt;The second post covered two years of running that in production.&lt;/p&gt;

&lt;p&gt;This post is about the month it stopped working.&lt;/p&gt;

&lt;p&gt;In January 2026 our heartbeat traffic went from boring to terrifying and stayed there for about four weeks. This is the story of what broke, why the original design had a ceiling we never noticed, and the changes that fixed it: &lt;strong&gt;more Event Hub partitions, Azure Container Apps, and KEDA&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Storm
&lt;/h2&gt;

&lt;p&gt;A normal day looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;51,000-58,000 heartbeats per hour&lt;/strong&gt;, hour after hour&lt;/li&gt;
&lt;li&gt;Roughly 15-16 events per second at idle&lt;/li&gt;
&lt;li&gt;Flat, predictable, forgettable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On &lt;strong&gt;January 5, around 7:00 AM PST&lt;/strong&gt;, it stopped being flat.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (PST)&lt;/th&gt;
&lt;th&gt;Heartbeats/hour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;~57,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12:00 PM&lt;/td&gt;
&lt;td&gt;80,005&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1:00 PM&lt;/td&gt;
&lt;td&gt;216,351&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5:00 PM&lt;/td&gt;
&lt;td&gt;343,480&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9:00 PM&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;466,760&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is &lt;strong&gt;eight times normal event volume in a single hour, and it was still climbing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Events were only half the story. SignalR connection counts told the other half. At the worst of it we were holding roughly &lt;strong&gt;eleven times&lt;/strong&gt; the connections we normally maintain, and every one of those was a browser session we had to track, keep alive, and report status for.&lt;/p&gt;

&lt;p&gt;It did not spike and recover. It stayed elevated for weeks while we hunted for the cause.&lt;/p&gt;

&lt;p&gt;When we finally found it, the answer was almost funny: &lt;strong&gt;507 zombie sessions&lt;/strong&gt; that never ended, running months-old cached client code, and a &lt;strong&gt;single user account responsible for 33% of all our token API traffic&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One account. Eight times the load. Four weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Eight Times Load Actually Did
&lt;/h2&gt;

&lt;p&gt;Here is the part that matters, and it has nothing to do with the number itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our Event Hub had one partition.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Event Hubs, the partition count sets how much you can read in parallel. Within a consumer group, each partition is owned by one processor at a time.&lt;/p&gt;

&lt;p&gt;One partition means one owner. Everything else waits its turn. More instances, more CPU, better code, none of it helps. There is only one lane.&lt;/p&gt;

&lt;p&gt;So while events poured in at well over a hundred per second, HeartbeatMonitor processed them single-file. A handful per second. Sometimes it felt like one.&lt;/p&gt;

&lt;p&gt;Incoming and outgoing stopped matching, and the backlog only grew. Facility connectivity status, the thing hospitals and EMS actually look at, started lagging reality.&lt;/p&gt;

&lt;p&gt;Then the Consumption plan added its own failure mode: &lt;strong&gt;SNAT port exhaustion.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Under sustained storm traffic, outbound connections tied up SNAT ports faster than they were released. We ran out. New connections slowed, then failed outright. Failed calls made clients retry. Retries made more load. More load exhausted more ports.&lt;/p&gt;

&lt;p&gt;We had built a feedback loop, and it was feeding itself.&lt;/p&gt;

&lt;p&gt;Consumption gave us nothing to work with either. No instance visibility, no per-instance control, no way to reason about what any single worker was doing. And since scale-out was capped by the partition count anyway, more instances would not have helped even if we could see them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pattern was fine. The plumbing under it had a ceiling of one.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Buying Time
&lt;/h2&gt;

&lt;p&gt;Before rewriting anything, we needed to survive.&lt;/p&gt;

&lt;p&gt;That heartbeat interval from the intro is a variable, and the API hands it to every client on every call. It normally sits at 30 seconds. We set it to &lt;strong&gt;90 seconds&lt;/strong&gt;, which cut incoming events by roughly two thirds. No deploy, no client update.&lt;/p&gt;

&lt;p&gt;It worked. It was also a tourniquet. Connectivity status got noticeably coarser, but it kept the lights on while we built the real fix.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Worth stealing regardless of your architecture: &lt;strong&gt;let the server tell the client how often to call.&lt;/strong&gt; That one design decision was the difference between a bad month and an outage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Fix 1 - Give the Queue More Lanes
&lt;/h2&gt;

&lt;p&gt;On our Event Hub tier we could not change the partition count on the existing hub, so we created a new one: &lt;strong&gt;&lt;code&gt;heartbeat-p8&lt;/code&gt;&lt;/strong&gt;, with &lt;strong&gt;8 partitions&lt;/strong&gt; and a dedicated &lt;code&gt;heartbeat-monitor&lt;/code&gt; consumer group.&lt;/p&gt;

&lt;p&gt;That is the whole fix, and it is the foundation for everything after it. Eight partitions means eight streams can be processed in parallel instead of forcing all the work through one. A noisy workload has far less ability to stall the entire pipeline.&lt;/p&gt;

&lt;p&gt;Eight lanes only help if you have eight drivers, though. That is the next part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2 - Why KEDA
&lt;/h2&gt;

&lt;p&gt;KEDA watches how many unprocessed events are sitting in the Event Hub and scales the worker for you. No timers, no guessing, no manual scale-out.&lt;/p&gt;

&lt;p&gt;Here is the behavior that sold us. As lag grows, KEDA adds replicas. The Event Hub processor then spreads partition ownership across whatever replicas are running, each one checkpointing independently. Events fan out across partitions, replicas fan out to match.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgl00gsxnk7h900xny4cw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgl00gsxnk7h900xny4cw.png" alt=" " width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;maxReplicas&lt;/code&gt; to the partition count. This is the one number worth getting right.&lt;/p&gt;

&lt;p&gt;There are only eight partitions to own, so a ninth replica has nothing to take. At best it sits idle. At worst it adds avoidable rebalancing, which means brief churn and duplicate processing from the last checkpoint. Extra replicas do not buy throughput.&lt;/p&gt;

&lt;p&gt;All of it lives in Bicep, so every environment gets the same behavior with different numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scale: {
  minReplicas: minReplicas
  maxReplicas: maxReplicas
  rules: [
    {
      name: 'eventhub-keda-rule'
      custom: {
        type: 'azure-eventhub'
        metadata: {
          consumerGroup: toLower(eventHubConsumerGroup)
          unprocessedEventThreshold: kedaUnprocessedEventThreshold
          activationUnprocessedEventThreshold: '0'
          checkpointStrategy: 'blobMetadata'
          blobContainer: 'heartbeat-checkpoints'
        }
        auth: [
          {
            secretRef: 'eventhub-connection'
            triggerParameter: 'connection'
          }
          {
            secretRef: 'storage-connection'
            triggerParameter: 'storageConnection'
          }
        ]
      }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things in there cost us real time, so take them for free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;checkpointStrategy: 'blobMetadata'&lt;/code&gt; - get this wrong and KEDA reads your lag as zero and never scales&lt;/li&gt;
&lt;li&gt;If an old Function App is still running against the same hub and consumer group, it keeps competing for partition ownership and the Container App never settles. Stop it first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Non-production environments run 2 partitions with &lt;code&gt;minReplicas: 0&lt;/code&gt;, so they scale to zero and cost nothing when nobody is testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3 - Make the Image Small Enough to Matter
&lt;/h2&gt;

&lt;p&gt;Fast scale-out is a lie if your container takes 45 seconds to start.&lt;/p&gt;

&lt;p&gt;So the HeartbeatMonitor image got stripped down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No ingress.&lt;/strong&gt; It is a worker. Nothing calls it. It has no reason to have a networking surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No health check libraries.&lt;/strong&gt; ACA probes handle liveness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No extra packages.&lt;/strong&gt; If it is not on the hot path between Event Hub and Redis, it is not in the image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a small image sitting in ACR, ready to go. During a storm, the time between "KEDA notices lag" and "a new instance is processing events" is measured in seconds, not minutes. That difference is the entire point.&lt;/p&gt;

&lt;p&gt;There is a quieter benefit too. Moving off Consumption to a container we control means we can actually see and tune each instance: logs, probes, resource limits, startup behavior. After a month of flying blind, that visibility was worth as much as the scaling.&lt;/p&gt;

&lt;h2&gt;
  
  
  SessionMonitor - A Different Job, Different Rules
&lt;/h2&gt;

&lt;p&gt;Not everything should scale out.&lt;/p&gt;

&lt;p&gt;SessionMonitor reconciles Redis session state against SQL and pushes facility status changes. Running two of them means doing the same work twice. So it moved to ACA as well, but &lt;strong&gt;fixed at exactly one instance&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What it got instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A speed knob in Redis.&lt;/strong&gt; The poll interval is read from Redis at runtime, not from config. Speed it up, slow it down, or effectively pause it, mid-incident, with no redeploy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli SET SessionMonitor:PollIntervalSeconds 15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Catch-up on startup.&lt;/strong&gt; Every session lives in Redis as two keys: a short-lived one that expires when the user goes quiet, and a long-lived one that sticks around as the record.&lt;/p&gt;

&lt;p&gt;The short key expiring is the signal that someone went offline. But Redis expiration events are fire-and-forget. If the container is down at that moment, the signal is gone and that session stays marked online forever. In the old Function App, that is exactly what happened.&lt;/p&gt;

&lt;p&gt;So SessionMonitor now starts by comparing the two sets of keys. Anything holding a long-lived key with no short-lived partner expired while we were away. It clears those first, then starts listening for live events.&lt;/p&gt;

&lt;p&gt;Simple idea, and it means a restart or a deploy no longer costs us accuracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving It
&lt;/h2&gt;

&lt;p&gt;Honest answer first: &lt;strong&gt;we could never reproduce the real storm in a load test.&lt;/strong&gt; Not even close. Four weeks of a broken client hammering production is not something you manufacture with JMeter on a Tuesday.&lt;/p&gt;

&lt;p&gt;So we stopped trying to recreate the storm and tested the thing that actually mattered, which is the response.&lt;/p&gt;

&lt;p&gt;The method was simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Stop the Container App entirely&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Run the JMeter load test for several minutes against the heartbeat API&lt;/li&gt;
&lt;li&gt;Let the queue pile up to &lt;strong&gt;tens of thousands of unprocessed events&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Start the Container App and watch&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Production normally keeps one replica running at all times. We stopped it on purpose so the test would show the worst case: a cold start into a full queue.&lt;/p&gt;

&lt;p&gt;KEDA drove it up to all eight replicas almost immediately. All eight partitions were consumed in parallel, the backlog drained, and once the queue was clear it scaled back down on its own.&lt;/p&gt;

&lt;p&gt;That is the behavior we needed to see. Not "can we survive a storm we already survived," but "when the backlog appears, does the system react instantly and without anyone being awake."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where We Landed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Production reads and writes to the queue &lt;strong&gt;stay in sync&lt;/strong&gt;, including under heavy load&lt;/li&gt;
&lt;li&gt;The heartbeat interval is back to normal, no tourniquet needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No SNAT port exhaustion&lt;/strong&gt; since the migration&lt;/li&gt;
&lt;li&gt;Processing capacity scales with demand instead of collapsing under it&lt;/li&gt;
&lt;li&gt;Every environment is defined in Bicep, so dev, QA, staging, training, and prod behave identically at different sizes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture from the first post, heartbeat to Event Hub to Redis expiration to SignalR, was never the problem. It held up for two years and it holds up today.&lt;/p&gt;

&lt;p&gt;It just needed more than one lane, and something smart enough to fill them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previous posts in this series:&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/anoushnet/user-connectivity-a-real-time-web-solution-for-online-and-offline-user-status-3ll2"&gt;User Connectivity: A Real-time Web Solution for Online and Offline User Status&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/anoushnet/user-connectivity-two-years-in-production-lessons-learned-and-new-patterns-3fpf"&gt;User Connectivity: Two Years in Production - Lessons Learned and New Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>kubernetes</category>
      <category>azure</category>
      <category>architecture</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Scaling API Access with Azure API Management: From Manual to Self-Service</title>
      <dc:creator>Anoush</dc:creator>
      <pubDate>Sun, 04 Jan 2026 22:03:40 +0000</pubDate>
      <link>https://dev.to/anoushnet/scaling-api-access-with-azure-api-management-from-manual-to-self-service-3299</link>
      <guid>https://dev.to/anoushnet/scaling-api-access-with-azure-api-management-from-manual-to-self-service-3299</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Working on a healthcare application using .NET and Azure. The backend is .NET Core with SQL Server and Azure Functions on Azure Cloud. This application is a B2B product, where we need to expose our APIs to external clients and users who integrate with our services.&lt;/p&gt;

&lt;p&gt;We came out first with a traditional approach where each client would get custom API endpoints and we generate a GUID id (API key) for each client/user. We store the ids in our database and based on API key we authorize and authenticate the clients. But as we scaled up hundreds/thousands of facilities and users, this became unmanageable. Also, it is not secure to have the main API endpoint accessible to public. We needed a better way.&lt;/p&gt;

&lt;p&gt;This article describes how I architected our API access to be scalable, self-service and more secure. Basically, going from a manually configured API setup to an automated multi-tenant API gateway that serves thousands of users with zero manual configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Our application needed to serve hundreds or thousands of external users across multiple facilities. Each facility needed access to our APIs to exchange data. But here's the challenge: each client has access to different APIs and needed different parameters to push and pull their data.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Facility A needs facilityId=5001&amp;amp;locationId=1001&lt;/li&gt;
&lt;li&gt;Facility B needs facilityId=5002&amp;amp;locationId=1002&lt;/li&gt;
&lt;li&gt;Facility C needs facilityId=5003&amp;amp;locationId=1003$specificParams=Ture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The traditional approach would be to create separate API id called API Key in the database and endpoints would access their data based on their API Key. Or worse, require each facility to pass their own parameters and trust they're sending the right ones. Neither scales. The first creates endpoint sprawl. The second creates security issues.&lt;/p&gt;

&lt;p&gt;We needed our users to sign up, set up their settings (set their facility, location and other parameters in UI) and choose their API endpoint that could serve everyone securely, with each user automatically getting their correct parameters without manual configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture and Setup
&lt;/h2&gt;

&lt;p&gt;We built our solution using three Azure services: Azure API Management (APIM), Azure Functions, and Azure Table Storage.&lt;br&gt;
APIM acts as our API gateway. It's the single entry point for all external users. Functions handle our business logic and subscription management. I used APIM to set up API policies. Table Storage holds each subscription's custom parameters.&lt;/p&gt;

&lt;p&gt;Here's how I set it up. In APIM, I created one API endpoint. This single endpoint serves all facilities. When a user calls this endpoint with their subscription key, APIM does all the heavy lifting automatically through a policy.&lt;/p&gt;

&lt;p&gt;I have a separate function that is fully dedicated to managing APIM subscriptions. I created two main APIs: one to manage subscription creation (POST /api/subscription/create) and one to retrieve subscription parameters (GET /api/subscription/{id}). The first one is called when users sign up. The second one is called by APIM's policy to get that user's parameters.&lt;/p&gt;

&lt;p&gt;In Table Storage, we store a simple table called ApiSubscriptions with three columns: SubscriptionId (the primary key), SubscriptionKey, and Parameters. When a user signs up, I store their parameters here as a query string like facilityId=5001&amp;amp;locationId=1001.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzy28dhtegn8g56wxy0i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzy28dhtegn8g56wxy0i.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Flow
&lt;/h2&gt;

&lt;p&gt;When a user calls our API, here's what happens automatically:&lt;br&gt;
The facility sends a GET request to &lt;a href="https://api.example.com/data/ids" rel="noopener noreferrer"&gt;https://api.example.com/data/ids&lt;/a&gt; with their subscription key in the header: Ocp-Apim-Subscription-Key: abc123...&lt;br&gt;
APIM receives the request and validates the subscription key. If it's invalid, the request is rejected immediately.&lt;/p&gt;

&lt;p&gt;Once validated, APIM executes our policy. The policy does three things in sequence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First&lt;/strong&gt;, it calls Azure AD to get an OAuth token. This token is needed to call our backend Function App securely. APIM makes a POST request to &lt;a href="https://login.microsoftonline.com/%7Btenant%7D/oauth2/v2.0/token" rel="noopener noreferrer"&gt;https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token&lt;/a&gt; with client credentials. Azure AD returns a bearer token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second&lt;/strong&gt;, APIM calls our Function App to get this subscription's parameters. It makes a GET request to /api/subscription/{subscriptionId} with the bearer token in the Authorization header. The Function App looks up the subscription in Table Storage and returns the parameters: facilityId=5001&amp;amp;locationId=1001.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third&lt;/strong&gt;, APIM appends these parameters to the main backend API call. It rewrites the URL from /data/ids to /data/ids?code={functionCode}&amp;amp;facilityId=5001&amp;amp;locationId=1001 and forwards the request to the Function App with the OAuth token.&lt;/p&gt;

&lt;p&gt;The Function App processes the request with parameters and returns the facility's requested data. APIM forwards this response back to the user.&lt;br&gt;
All of this happens in milliseconds. The facility doesn't know any of this is happening. They just send their subscription key (in the header of the call) and get their data back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fji7omdsp2gv13dpd8nqb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fji7omdsp2gv13dpd8nqb.png" alt="Azure architecture diagram showing APIM policy flow with 3 steps: OAuth token retrieval, subscription parameter lookup, and backend API call with authentication" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The diagram shows the complete flow from API Client through APIM to backend services with OAuth authentication and dynamic parameter injection.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The APIM Policy&lt;/strong&gt;&lt;br&gt;
The entire flow is controlled by an APIM policy. This is an XML-based configuration that runs for every API request. Here's what our policy looks like, broken down into the three main steps:&lt;br&gt;
&lt;strong&gt;Step 1: Get OAuth Token&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;send-request mode="new" response-variable-name="tokenResponse" timeout="10"&amp;gt;
    &amp;lt;set-url&amp;gt;https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token&amp;lt;/set-url&amp;gt;
    &amp;lt;set-method&amp;gt;POST&amp;lt;/set-method&amp;gt;
    &amp;lt;set-header name="Content-Type"&amp;gt;
        &amp;lt;value&amp;gt;application/x-www-form-urlencoded&amp;lt;/value&amp;gt;
    &amp;lt;/set-header&amp;gt;
    &amp;lt;set-body&amp;gt;grant_type=client_credentials&amp;amp;client_id={id}&amp;amp;client_secret={secret}&amp;amp;scope={id}/.default&amp;lt;/set-body&amp;gt;
&amp;lt;/send-request&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This calls Azure AD and gets back an access token. We store it in a variable called tokenResponse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Get Subscription Parameters&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;send-request mode="new" response-variable-name="subscriptionResponse" timeout="10"&amp;gt;
    &amp;lt;set-url&amp;gt;@($"https://{function-app}/api/subscription/{context.Subscription.Id}?code={code}")&amp;lt;/set-url&amp;gt;
    &amp;lt;set-method&amp;gt;GET&amp;lt;/set-method&amp;gt;
    &amp;lt;set-header name="Authorization"&amp;gt;
        &amp;lt;value&amp;gt;@("Bearer " + accessToken)&amp;lt;/value&amp;gt;
    &amp;lt;/set-header&amp;gt;
&amp;lt;/send-request&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This calls our Function App to get the parameters. Notice we're using context.Subscription.Id which is the unique subscription identifier in APIM. We send the OAuth token in the Authorization header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Call Backend with Parameters&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;set-header name="Authorization"&amp;gt;
    &amp;lt;value&amp;gt;@("Bearer " + accessToken)&amp;lt;/value&amp;gt;
&amp;lt;/set-header&amp;gt;

&amp;lt;rewrite-uri template="@{
    string parameters = context.Variables["parameters"];
    return $"/api/data?code={code}&amp;amp;{parameters}";
}" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This appends the parameters to the backend call and sets the OAuth token in the Authorization header. The backend API receives the request with both authentication and the facility's specific parameters.&lt;/p&gt;

&lt;p&gt;That's the entire policy. Three steps that run automatically for every request.&lt;/p&gt;

&lt;p&gt;I also have other types of APIM Policies. For example, one that I use often is the rate-limit policy. That protects our APIs when there are a lot of calls during a short amount of time. This built-in policy will give "Too Many Requests" (HTTP 429) when that occurs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subscription Management
&lt;/h2&gt;

&lt;p&gt;We built a Function App to handle subscription lifecycle. It has two main APIs:&lt;/p&gt;

&lt;p&gt;Create Subscription (POST /api/subscription/create)&lt;/p&gt;

&lt;p&gt;This function does four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creates the user in APIM (if they don't exist)&lt;/li&gt;
&lt;li&gt;Creates the subscription in APIM for the selected product&lt;/li&gt;
&lt;li&gt;Stores the subscription parameters in Table Storage&lt;/li&gt;
&lt;li&gt;Returns the subscription key and ID to the user&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the data flow: user submits their email, name, product selection, and facility selection through our signup portal. The Function App creates an APIM user using their email as the identifier. Then it creates a subscription for that user to our API product. Then it stores the subscription ID and parameters (facilityId=X&amp;amp;locationId=Y) in Table Storage. Finally, it returns the subscription key to the user.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Get Subscription *&lt;/em&gt;(GET /api/subscription/{id})&lt;/p&gt;

&lt;p&gt;This function is called by the APIM policy. It takes a subscription ID, looks it up in Table Storage, and returns the parameters. Simple lookup operation.&lt;/p&gt;

&lt;p&gt;Both functions use OAuth authentication. Only APIM can call them because APIM uses Managed Identity to get a token from Azure AD.&lt;/p&gt;

&lt;p&gt;One important configuration: the Function App's Managed Identity must authenticate to the correct Azure AD tenant. I explicitly set the tenant ID in the DefaultAzureCredential to ensure it gets tokens from the right Azure AD instance. Without this, authentication fails with a tenant mismatch error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer Portal
&lt;/h2&gt;

&lt;p&gt;I built two signup portals: one integrated into the APIM Developer Portal and one standalone static website hosted on Azure Storage.&lt;/p&gt;

&lt;p&gt;In the APIM Developer Portal, I created a custom page called "Create Subscription". Users can access this page, fill out a form with their email, name, product selection, facility selection, other settings and click "Create Subscription". The form calls our Function App's create endpoint, and the user immediately gets their subscription key displayed on screen.&lt;/p&gt;

&lt;p&gt;The standalone portal is a simple HTML page with JavaScript. It does the same thing but can be shared with external users who don't need to log into the APIM portal. We use this for quick onboarding by email invitations.&lt;/p&gt;

&lt;p&gt;Both portals load the facility list dynamically from another Function App endpoint (GET /api/facilities) which queries our database for active facilities. It also lets users choose their APIM Product or API Group.&lt;br&gt;
For the portals to call the Function App from the browser, I configured CORS in the APIM policy. I set allowed-origins to wildcard (*) for the POC. For production, this should be locked down to specific portal URLs only.&lt;/p&gt;

&lt;p&gt;When the user submits the form, JavaScript calls POST /api/subscription/create with the payload example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "email": "user@example.com",
  "name": "John Doe",
  "product": "example-apis",
  "facilityId": "5001",
  "locationId": "1001"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Function App does all the work and returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "subscriptionKey": "abc123...",
  "subscriptionId": "user-example-com-xyz789"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The portal displays this to the user with instructions on how to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This architecture solved our multi-tenant API access problem, without admin involvement and separates authentication and authorization using APIM. Each user gets their correct parameters automatically without manual configuration.&lt;/p&gt;

&lt;p&gt;The key components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;APIM handles routing and policy execution&lt;/li&gt;
&lt;li&gt;Azure Functions manage subscriptions and business logic&lt;/li&gt;
&lt;li&gt;Table Storage holds subscription parameters&lt;/li&gt;
&lt;li&gt;OAuth secures all communications&lt;/li&gt;
&lt;li&gt;APIM Policy ties it all together with three simple steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building B2B APIs that need to serve multiple tenants with different parameters, this pattern works. It's scalable, secure, and self-service.&lt;/p&gt;

&lt;p&gt;The full code and APIM policy are available in production. The system runs itself.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>azure</category>
      <category>api</category>
      <category>devops</category>
    </item>
    <item>
      <title>User Connectivity: Two Years in Production — Lessons Learned and New Patterns</title>
      <dc:creator>Anoush</dc:creator>
      <pubDate>Wed, 24 Dec 2025 06:23:54 +0000</pubDate>
      <link>https://dev.to/anoushnet/user-connectivity-two-years-in-production-lessons-learned-and-new-patterns-3fpf</link>
      <guid>https://dev.to/anoushnet/user-connectivity-two-years-in-production-lessons-learned-and-new-patterns-3fpf</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is a follow-up to my previous post on User Connectivity: A Real-time Web Solution for Online and Offline User Status. It has been almost two years since I first deployed my user connectivity solution to production. What started as an experiment has become a cornerstone of my architecture.&lt;br&gt;
Could I make it simpler? Yes. However, the time and effort for an upgrade is not worth it given other priorities. The system has been rock solid, and I have expanded its use to solve other challenges in my application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expanding the Pattern: Solving Database Deadlocks
&lt;/h2&gt;

&lt;p&gt;Recently, I encountered complex database deadlock issues caused by several API processes. After analyzing the problem, I recognized an opportunity to apply my event-driven architecture. By separating out specific tasks and processing them asynchronously, I eliminated the deadlocks entirely.&lt;br&gt;
This led me to create a centralized service I call the Event Distributor.  An Azure Function App responsible solely for listening to Redis expiration events and forwarding them to an Azure Event Grid queue. Multiple Azure Function App workers then process events from this queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event Distributor Architecture
&lt;/h2&gt;

&lt;p&gt;The goal of the Event Distributor is simple: one central service that listens to Redis expiration events and nothing else.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw956yvzss27amig3nkc2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw956yvzss27amig3nkc2.png" alt=" " width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This separation provides several benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Traceability&lt;/strong&gt;: All events pass through a single point, making it easier to trace issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging&lt;/strong&gt;: Every received event is logged to an Azure Cosmos DB with its status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Worker functions can scale independently based on queue depth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt;: A single, well-monitored listener reduces the risk of missed events.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Observations: Azure Services
&lt;/h2&gt;

&lt;p&gt;After two years of running this architecture, I have two critical observations for anyone implementing a similar solution with Azure services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Use Premium Plan for Redis Listeners&lt;/strong&gt;&lt;br&gt;
If your Azure Function App uses the RedisPubSubTrigger to listen for Redis expiration events, you must use the Premium plan. The Consumption plan is not reliable for this use case.&lt;/p&gt;

&lt;p&gt;Why? The Consumption plan has cold start delays and limited connection lifetimes. When the function app is idle, it can go to sleep and miss events from Redis. With the Premium plan, your function app is always on with long-lived connections — no cold starts, no missed events. I have not seen a single lost event since moving to Premium.&lt;/p&gt;

&lt;p&gt;You may ask: how did the original user connectivity solution work on the Consumption plan?&lt;br&gt;
The answer is traffic volume. Even in idle mode, my application generates 170–200 heartbeat events per second. During peak usage, this increases 6–10 times. The function app never goes to sleep because of this constant traffic. Additionally, my Heartbeat function app includes both the RedisPubSubTrigger and HTTP triggers, keeping it active.&lt;br&gt;
For a dedicated Event Distributor that only listens to Redis expiration events, the Premium plan is essential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Azure Managed Redis Limitation&lt;/strong&gt;&lt;br&gt;
Azure has introduced a new Redis service called Azure Managed Redis (formerly Redis Enterprise). This service runs on a clustered architecture with multiple Redis servers under the hood.&lt;/p&gt;

&lt;p&gt;However, there is a significant limitation: these servers cannot be fully configured to send expiration events reliably. When the underlying servers restart or cycle, they lose their configuration and stop sending expiration events.&lt;/p&gt;

&lt;p&gt;I have reported this issue to Microsoft. They confirmed it is on their backlog, but as of today, there is no solution. If your architecture depends on Redis key expiration events, be aware of this limitation before migrating to Azure Managed Redis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;My event-driven architecture has proven itself over two years of production use. It solved my original user connectivity requirements and has since helped me address complex database deadlock issues. The key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the Premium plan for Redis listeners&lt;/li&gt;
&lt;li&gt;Be cautious with Azure Managed Redis if you rely on expiration events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope this helps others take advantage of this architecture.&lt;/p&gt;

</description>
      <category>microsoft</category>
      <category>eventdriven</category>
      <category>softwareengineering</category>
      <category>architecture</category>
    </item>
    <item>
      <title>User Connectivity: A Real-time Web Solution for Online and Offline User Status</title>
      <dc:creator>Anoush</dc:creator>
      <pubDate>Tue, 06 Feb 2024 06:46:08 +0000</pubDate>
      <link>https://dev.to/anoushnet/user-connectivity-a-real-time-web-solution-for-online-and-offline-user-status-3ll2</link>
      <guid>https://dev.to/anoushnet/user-connectivity-a-real-time-web-solution-for-online-and-offline-user-status-3ll2</guid>
      <description>&lt;p&gt;&lt;strong&gt;Introduction:&lt;/strong&gt;&lt;br&gt;
This post explores user connectivity in a web application, focusing on displaying the real-time connectivity status of facilities. We'll discuss how users are associated with facilities, walk through our setup, communication methods with connected sessions, and our approach to ensuring a stable and reliable solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connectivity Status:&lt;/strong&gt;&lt;br&gt;
In our context, connectivity status refers to whether a facility is online or offline. This status indicates whether one or multiple users associated with a facility are actively logged into our application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application Setup and Details:&lt;/strong&gt;&lt;br&gt;
Our application operates in real-time, using sessions associated with users. Users are linked to specific facilities, and each facility is associated with a distinct region or county.&lt;br&gt;
The core of our real-time functionality involves notifying all relevant sessions/users about the facility's connectivity status in real-time, delivered to the user's browser using SignalR (Web Sockets protocol) event messages.&lt;br&gt;
We host our application on Microsoft Azure Cloud, utilizing the following technology stack and &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Azure Cloud Services:&lt;/strong&gt;&lt;br&gt;
Technology Stack:&lt;br&gt;
Front-end: JavaScript/Angular&lt;br&gt;
Back-end: .Net Core&lt;br&gt;
Database: Azure SQL Server&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Azure Services:&lt;/strong&gt;&lt;br&gt;
SignalR&lt;br&gt;
Azure Event Hub&lt;br&gt;
Redis Cache&lt;br&gt;
Azure Functions&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;br&gt;
Now, let's dive into the specifics of our solution.&lt;br&gt;
To establish real-time user connectivity, our front-end application regularly sends heartbeat signals through simple Post API calls every 30 seconds. The heartbeat events are stored in an Azure Event Hub Queue, which can handle millions of events per second.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe6ae7qqtwh3ptlukwwln.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe6ae7qqtwh3ptlukwwln.png" alt="Heartbeat API Calls" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heartbeat Event Process:&lt;/strong&gt;&lt;br&gt;
We utilize a worker service called the Heartbeat Monitor (implemented as Azure Functions) to read and store heartbeat events in Redis Cache. The events are stored in two lists: a Redis expiring list called Session list and another list called Session Value list, containing heartbeat event values.&lt;br&gt;
Each time a heartbeat event is read, it is fetched from the Session list. If it does not exist, the session element is transactionally created in both lists to ensure synchronization. If the session already exists, the Session End Time is set to the expiration time in the Session Value list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffv3jqj0ky1ic13le3sy8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffv3jqj0ky1ic13le3sy8.png" alt="Heartbeat Monitor Process" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expired Session Callback:&lt;/strong&gt;&lt;br&gt;
To manage session expiration, we listen to Redis expired keys and execute a function that processes the session and sets it to offline with a timestamp. This is done by configuring Redis to send events and listening to a specific event: __keyevent@0:expired.&lt;/p&gt;

&lt;p&gt;However, Microsoft has introduced a new Azure Function Trigger, &lt;a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-cache-trigger-redispubsub?tabs=isolated-process%2Cnode-v3%2Cpython-v1&amp;amp;pivots=programming-language-csharp" rel="noopener noreferrer"&gt;"RedisPubSubTrigger"&lt;/a&gt;, which captures the expiration (__keyevent@0:expired) of a session more efficiently.&lt;/p&gt;

&lt;p&gt;When a session expires, this listner function sets the session's offline timestamp in the session value list, which is set to null while session in online.&lt;/p&gt;

&lt;p&gt;By listening to Redis expired keys, we execute function that process the session and set the session to be offline with timestamp.  This was originally done by configuring the Redis to send events and listening to specific event "__keyevent@0:expired".  However, Microsoft has new Azure Function Trigger &lt;a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-cache-trigger-redispubsub?tabs=isolated-process%2Cnode-v3%2Cpython-v1&amp;amp;pivots=programming-language-csharp" rel="noopener noreferrer"&gt;"RedisPubSubTrigger"&lt;/a&gt; that capture the expire session.  The session offline timestamp been set in the session value list, there after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session Monitor:&lt;/strong&gt;&lt;br&gt;
Another timer-based Azure Functions worker service runs every minute, processing each session in the Session Value list, associating sessions with facilities. After processing all sessions, the Session Monitor updates facility connectivity status, stores data in its private database, and removes offline sessions from the Session Value list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Process Facility Session:&lt;/strong&gt;&lt;br&gt;
Moving from the Session Monitor's role, let's explore the 'Process Facility Session' step. Here, we update facility connectivity status and perform tasks for seamless user connectivity. The Session Monitor submits events to proper users or regions/counties, updates facility data in its database, and updates the facility status in the core database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fewv7pej7jbceaip1tx8d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fewv7pej7jbceaip1tx8d.png" alt="Session Monitor Process" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Conclusion:&lt;br&gt;
The facility session process is highly efficient, processing only the updated facilities whose connectivity status has changed. It summarizes all sessions to the facility, avoiding the need to send events per session and reducing processing per browser connection.&lt;br&gt;
Using Redis expiration keys is reliable and stable, streamlining functionality and code by eliminating the need to individually process each session in the list to determine its online or offline status.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update (December 2024):&lt;/strong&gt;&lt;br&gt;
This architecture has been running successfully in production for over two years. I have published a follow-up article covering lessons learned, a new Event Distributor pattern, and important Azure service considerations: User Connectivity: Two Years in Production — Lessons Learned and New Patterns&lt;/p&gt;

</description>
      <category>azure</category>
      <category>onlineoffline</category>
      <category>api</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Going Desktop to Mobile view by configuring the Angular router</title>
      <dc:creator>Anoush</dc:creator>
      <pubDate>Tue, 09 Jul 2019 03:22:01 +0000</pubDate>
      <link>https://dev.to/anoushnet/going-desktop-to-mobile-view-by-configuring-the-angular-router-4087</link>
      <guid>https://dev.to/anoushnet/going-desktop-to-mobile-view-by-configuring-the-angular-router-4087</guid>
      <description>&lt;p&gt;Working on an emergency application (with live data) using .net and angular.  The backend is .net with SQL server and SignalR on Azure.  This application is sales driven product, we came out first with desktop version and then mobile friendly version due to way the market and 90-80% of users are on desktop view.&lt;/p&gt;

&lt;p&gt;This article describes how we architect our application do be responsive.  Basically, going from an already existing desktop web application to responsive application.  When referring to word “mobile” in article, it is not a mobile native application or Progressive Web App, it is referring to responsive application on different tables and mobile devices.&lt;/p&gt;

&lt;h1&gt;
  
  
  Architecture and Setup
&lt;/h1&gt;

&lt;p&gt;We started by in our angular application under root folder “src”.  We created a folder called “mobile”.  We have the “app” folder for main desktop view, it can be renamed to “desktop”.  This gave us a whole new place to have our mobile code and not been in the same folders with all other code, it is more organized, clean and separated from all other code.&lt;/p&gt;

&lt;p&gt;In app router module “AppRoutingModule”, when application get loaded.  We load an array of all paths of the application, from “app” and “mobile” folders.  When application knows the size of window “window.innerWidth” it will modify the path array and rests the route configuration by calling "this.route.resetConfig()".  Here we also listen to “window:resize” in the main component of the application for window resizing, and the router path gets modified and then router configuration gets reloaded.  So, user can go from desktop to mobile view and vice and versa.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;desktopPreloadModules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;loadChildren&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app/modules/dashboard/dashboard.module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mod&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DashboardModule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;debug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;loadChildren&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app/modules/debug/debug.module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mod&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DebugModule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mobilePreloadModules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;loadChildren&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mobile/modules/dashboard/dashboard.mobile.module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mod&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DashboardMobileModule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;debug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;loadChildren&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mobile/modules/debug/debug.mobile.module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mod&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DebugMobileModule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appRoutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LoginComponent&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MainComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;canActivate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AuthGuard&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppDataResolver&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;desktopPreloadModules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;mobilePreloadModules&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;redirectTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pathMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LoginComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;NgModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;RouterModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appRoutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preloadingStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SelectiveStrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;enableTracing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;scrollPositionRestoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;SelectiveStrategy&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;RouterModule&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppRoutingModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also CSS files that styling changes depending on the window size, we used media query for that, by calling:&lt;br&gt;
&lt;br&gt;
 &lt;code&gt;@media only screen and (max-width: ####px)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;In our application we also have some dynamic dialog boxes, we also override those components by reloading them either mobile view or desktop view.&lt;/p&gt;

&lt;p&gt;After route is change the all components (that is in the current view) needs to be reloaded.  Here, we solve this by simply navigating to and empty path this basically reloads all current component in the DOM (it is not refresh from server.)&lt;/p&gt;

&lt;h1&gt;
  
  
  Mobile Implementation
&lt;/h1&gt;

&lt;p&gt;In the mobile for with similar path tree as desktop “app” folder, by using OOP inheritance, polymorphism and overriding methods.  We could access all of the functionalities from desktop services and components and reused the code, by extending from same components.  Following the open/closed principle.  For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-mobile-component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./my.mobile.component.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyMobileComponent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;MyDesktopComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In case mobile functionality is different than desktop, we override the desktop method in the mobile folder.&lt;/p&gt;

&lt;p&gt;In our application which is medium to advanced application complexity.  There is only one place that it is checking if application is mobile or desktop and that is in the “AppRoutingModule” there no “if (mobile) do this else do that” outside the router.  We follow and stick with this pattern due to the fact that the application could be very messy.  If we have a mobile specific component or function with extending the class from desktop and override or add mobile specific functions or view.  Following the single responsibility principle.&lt;/p&gt;

&lt;h1&gt;
  
  
  Pros
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Clean and organized code.&lt;/li&gt;
&lt;li&gt;Easy to implement the mobile view application.&lt;/li&gt;
&lt;li&gt;Reuse of the code, this saves us lots of time to impalement and test.  The desktop code is already in production and been used by users, we know if works.  So, we are confident that code works.&lt;/li&gt;
&lt;li&gt;Follows the single responsibility principle&lt;/li&gt;
&lt;li&gt;Follows the open/closed principle&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Cons
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;If we want to override a desktop behavior or functionality we will need to extend it in mobile.  This can give a little complexity to some deeper functionalities.  However, doable by refactoring and using SOLID principles.&lt;/li&gt;
&lt;li&gt;Can easily get a messy code if we breaking the pattern if someone writes “if (mobile) do this else do that” way or some other way.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>architecture</category>
      <category>responsive</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
