Category Archives: ASP.NET

Let’s talk Server-Sent Events

Server-Sent Events (SSE) are a lightweight streaming/push technology for pushing updates to a web client over a single long-live HTTP connection.

For example as use case where your web client connects to a server and periodically receives events or notifications from the server. Alternates to SSE might be long polling the server for updates, web sockets, Signal R etc. SSE is simpler than setting up web sockets and for C#/.NET developers can be seen to be similar to asynchronous streams.

All modern browser should support SSE using EventSource. SSE also supports automatic reconnection but one key thing to remember is this is only for one-way updates (unlike Signal R).

ASP.NET Sample

Let’s create an ASP.NET server sample with an endpoint /events of type text/event-stream. This will just look and send “ticks” to a client.

app.MapGet("/events", async (HttpContext context, CancellationToken cancellationToken) =>
{
    context.Response.ContentType = "text/event-stream";
    context.Response.Headers.CacheControl = "no-cache";

    try
    {
        for (var i = 1; i <= 20; i++)
        {
            var payload = JsonSerializer.Serialize(new
            {
                id = i,
                message = $"Server tick {i}",
                sentAtUtc = DateTimeOffset.UtcNow
            });

            await context.Response.WriteAsync($"id: {i}\n", cancellationToken);
            await context.Response.WriteAsync("event: tick\n", cancellationToken);
            await context.Response.WriteAsync($"data: {payload}\n\n", cancellationToken);
            await context.Response.Body.FlushAsync(cancellationToken);

            await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
        }
    }
    catch (OperationCanceledException)
    {
    }
});

Pretty simple. As mentioned we need to set the ContentType to text/event-stream. SSE must never be cached, hence context.Response.Headers.CacheControl = “no-cache”. The handler should not return until all responses are sent, i.e. it goes into a loop or similar and writes responses.

It’s also important to note that the SSE spec requires the following wire format

  • id: An optional event ID
  • event: An optional even name
  • datae: The actual payload
  • Ends with a blank line: Notice of the line setting the data we have two new lines, i.e. one blank line. Without this the browser will not dispatch the event
  • Flush after each event: This will force the server to push the event immediately instead of buffering, otherwise the client might receive events in chunks

C# client code

Let’s look at what’s required for a C# client to interact with our server.

The HttpClient looks like this

using var httpClient = new HttpClient
{
    BaseAddress = new Uri("http://localhost:5127")
};

Now we’ll created the request tot he /events endpoint and accept the text/event-stream i.e.

using var cancellation = new CancellationTokenSource();

using var request = new HttpRequestMessage(HttpMethod.Get, "/events");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));

using var response = await httpClient.SendAsync(
    request,
    HttpCompletionOption.ResponseHeadersRead,
    cancellation.Token);

response.EnsureSuccessStatusCode();

Okay, seo we’ve configured things and called the endpoint, now let’s look at how we might read the SSE

await using var stream = await response.Content.ReadAsStreamAsync(cancellation.Token);
using var reader = new StreamReader(stream);

while (!cancellation.IsCancellationRequested)
{
  var line = await reader.ReadLineAsync(cancellation.Token);

  if (line is null)
  {
    break;
  }

  if (line.Length == 0)
  {
    if (eventDataBuilder.Length > 0)
    {
      Console.WriteLine($"[{DateTime.Now:T}] id={eventId}, event={eventName}, data={eventDataBuilder}");
    }

    eventId = string.Empty;
    eventName = string.Empty;
    eventDataBuilder.Clear();
    continue;
  }

  if (line.StartsWith("id: ", StringComparison.Ordinal))
  {
    eventId = line[4..];
    continue;
  }

  if (line.StartsWith("event: ", StringComparison.Ordinal))
  {
    eventName = line[7..];
    continue;
  }

  if (line.StartsWith("data: ", StringComparison.Ordinal))
  {
    if (eventDataBuilder.Length > 0)
    {
      eventDataBuilder.Append('\n');
    }

    eventDataBuilder.Append(line[6..]);
}

In the example code, above, we read each line from the SSE and get each part until we’re ready to put the read data together to write to the console.

Typescript client

The following example is a React Typescript implementation of a simply UI with connect buttons etc. to connect to the events URL, using an EventSource we add a listener to and output the SSE’s as they arrive.

type TickEvent = {
  id: number
  message: string
  sentAtUtc: string
}

const maxItems = 50

function App() {
  const [events, setEvents] = useState<TickEvent[]>([])
  const [isConnected, setIsConnected] = useState(false)
  const [status, setStatus] = useState('Disconnected')
  const eventSourceRef = useRef<EventSource | null>(null)

  useEffect(() => {
    return () => {
      eventSourceRef.current?.close()
      eventSourceRef.current = null
    }
  }, [])

  const connect = () => {
    if (eventSourceRef.current) {
      return
    }

    setStatus('Connecting...')

    const eventSource = new EventSource(import.meta.env.VITE_SSE_URL ?? '/events')
    eventSourceRef.current = eventSource

    eventSource.onopen = () => {
      setIsConnected(true)
      setStatus('Connected')
    }

    eventSource.addEventListener('tick', (event) => {
      const messageEvent = event as MessageEvent<string>

      try {
        const payload = JSON.parse(messageEvent.data) as TickEvent
        setEvents((previous) => [payload, ...previous].slice(0, maxItems))
      } catch {
        setStatus('Received malformed event payload')
      }
    })

    eventSource.onerror = () => {
      setStatus('Connection lost or closed by server')
      setIsConnected(false)
      eventSource.close()
      eventSourceRef.current = null
    }
  }

  const disconnect = () => {
    eventSourceRef.current?.close()
    eventSourceRef.current = null
    setIsConnected(false)
    setStatus('Disconnected')
  }

  const clear = () => setEvents([])

  return (
    <main className="app">
      <h1>Server Sent Events Demo</h1>
      <p className="subtitle">React + TypeScript client for the ServerSentEventsSample API</p>

      <div className="controls">
        <button onClick={connect} disabled={isConnected}>Connect</button>
        <button onClick={disconnect} disabled={!isConnected}>Disconnect</button>
        <button onClick={clear} disabled={events.length === 0}>Clear</button>
      </div>

      <p className="status">
        Status: <strong>{status}</strong>
      </p>

      <ul className="event-list">
        {events.length === 0 && <li className="empty">No events yet.</li>}
        {events.map((item) => (
          <li key={`${item.id}-${item.sentAtUtc}`}>
            <div className="row">
              <span className="id">#{item.id}</span>
              <span>{item.message}</span>
            </div>
            <time dateTime={item.sentAtUtc}>
              {new Date(item.sentAtUtc).toLocaleTimeString()}
            </time>
          </li>
        ))}
      </ul>
    </main>
  )
}

Source Code

Full code for these examples is available on Github.

Looking at security features and the web

Let’s take a look at various security features around web technologies, although I’ll we concentrating on their use in ASP.NET, but the information should be valid for other frameworks etc.

Note: We’ll look at some code for implementing this in a subsequent set of posts.

Authentication and Authorization

We’re talking Identity, JWT, OAuth, Open ID Connect.

Obviously the use of proper authentication and authorisation ensure only legitimate users have access to resources and forcing a least privilege and role based access ensures authenticated users can only access resources befitting their privileges.

OWASP risks mitigation:

  • A01 Broken Access Control and improper enforcement of permissions
  • A07 Identification and Authentication failures, weak of missing authentication flows
    • Data protection API (DPAPI) / ASP.NET Core Data Protection

      This is designed to protect “data at reset”, such as cookies, tokens CSRF keys etc. and providers key rotation and encryption services.

      OWASP risks mitigation:

      • A02 Cryptographic failures, weak or missing encryption of sensitive data
        • HTTPS Enforcement and HSTS

          This forces encrypted transport layers and prevents protocol downgrade attacks.

          OWASP risks mitigation:

          • A02 Cryptographic failures, sensitive data exposure
          • A05 Security misconfigurations, missing TLS or insecure defaults
            • Anti-Forgery Tokens (CSRF Protection)

              This prevents cross site request forgery by validation of user intent.

              OWASP risks mitigation:

              • A01 Broken access control
              • A05 Security misconfigurations
              • A08 Software and Data integrity failures such as session integrity
                • Input Validation and Model Binding Validation

                  This prevents malformed or malicious input from reaching the business logic.

                  OWASP risks mitigation:

                  • A03 Injection, such as SQL, NoSQL and command injections
                  • A04 Insecure design, lacking validation rules
                  • A05 Security misconfigurations
                    • Output Encoding

                      This prevents untrusted data from being rendered, for example covers things like Razor, Tag Helpers, HTML Encoders.

                      OWASP risks mitigation:

                      • A03 Injection
                      • A05 Security misconfigurations
                      • A06 Vulnerable and outdated components
                        • Security Headers

                          Covers things such as CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy and mitigates XSS, click jacking, MIME sniffing and data leakage.

                          OWASP providers explicit guidance on recommended headers.

                          OWASP risks mitigation:

                          • A03 Injection, CSP reduces XSS
                          • A05 Security misconfigurations, missing headers
                          • A09 Security logging and monitoring failures, via reporting endpoints
                            • Rate limiting and throttling

                              This is included, but need to be enabled as ASP.NET built in middleware.

                              This prevents brute force, credential stuffing and resource exhaustion attacks.

                              OWASP risks mitigation:

                              • A07 Identification and Authentication failures
                              • A10 Server side request forgery (SSRF) limit abuse
                              • A04 Insecure design, lack of abuse protection
                                • CORS (Cross‑Origin Resource Sharing)

                                  This controls which origins can access API’s and prevents unauthorized cross-site API calls.

                                  OWASP risks mitigation:

                                  • A05 Security misconfiguration
                                  • A01 Broken access control
                                    • Cookie Security

                                      Protects session cookies from theft or misuse.

                                      OWASP risks mitigation:

                                      • A07 Identification and Authentication failures
                                      • A02 Cryptographic Failures
                                      • A01 Broken access control
                                        • Dependency Management

                                          When using third party dependencies via NuGet, NPM etc. we need to ensure libraries are patched and up to date.

                                          OWASP risks mitigation:

                                          • A06 Vulnerable and outdated components
                                            • Logging and Monitoring

                                              This covers things like Serilog, Application Insights and built-in logging etc.

                                              Used to detect suspicious activites, as well as support incident response.

                                              OWASP risks mitigation:

                                              • A09 Security Logging and monitoring failures
                                                • Secure deployment and configuration

                                                  This covers all forms of configuration, including appsettings.json, key vault, environment seperation etc.

                                                  Here we want to prevent secrets being exposed and enforce secure defaults.

                                                  OWASP risks mitigation:

                                                  • A05 Security misconfiguration
                                                  • A02 Cryptographic Failures

Adding a WebApi controller to an existing ASP.NET MVC application

So I’ve got an existing ASP.NET MVC5 application and need to add a REST api using WebApi.

  • Add a new Controller
  • Select Web API 2 Controller – Empty (or whatever your preference is)
  • Add you methods as normal
  • Open Global.asax.cs and near the start, for example after AreaRegistration but before the route configuration, add
    GlobalConfiguration.Configure(WebApiConfig.Register);
    

easy enough. The key is to not put the GlobalConfiguration as the last line in the Global.asax.cs as I did initially.

If we assume your controller was named AlbumsController, it might looks something like this

public class AlbumsController : ApiController
{
   // api/albums
   public IEnumerable<Album> GetAllAlbums()
   {
      // assuming albums is populated 
      // with a list of Album objects
      return albums;
   }
}

as per the comment, access to the API will be through url/api/albums, see WebApiConfig in App_Start for the configuration of this URL.

Passing arguments to an ASP.NET MVC5 controller

In our controller we might have a method along the lines

public string Search(string criteria, bool ignoreCase = true)
{
   // do something useful
   return $"Criteria: {criteria}, Ignore Case: {ignoreCase}";
}

Note: I’ve not bothered using HttpUtility.HtmlEncode on the return string as I want to minimize the code for these snippets.

So we can simply create a query string as per

http://localhost:58277/Music/Search?criteria=something&ignoreCase=false

or we can add/change the routing in RouteConfig, so for example in RouteConfig, RegisterRoutes we add

routes.MapRoute(
   name: "Music",
   url: "{controller}/{action}/{criteria}/{ignoreCase}"
);

now we can compose a URL thus

http://localhost:58277/Music/Search/something/false

Note: the routing names /{criteria}/{ignoreCase} must have the same names as the method parameters.

Obviously this example is a little contrived as we probably wouldn’t want to create a route for such a specific method signature.

We might simply incorporate partial parameters into the routine, for example maybe all our MusicController methods took a citeria argument then we might use

routes.MapRoute(
   name: "Music",
   url: "{controller}/{action}/{criteria}"
);

Note: there cannot be another route with the same number of parameters in the url preceding this or it will not be used.

and hence our URL would like like

http://localhost:58277/Music/Search/something?ignoreCase=false

ASP.NET MVC and IoC

This should be a nice short post.

As I use IoC a lot in my desktop applications I also want similar capabilities in an ASP.NET MVC application. I’ll use Unity as the container initally.

  • Create a new project using the Templates | Web | ASP.NET Web Application option in the New Project dialog in Visual Studio, press OK
  • Next Select the MVC Template and change authentication (if need be) and check whether to host in the cloud or not, then press OK
  • Select the References section in your solution explorer, right mouse click and select Manage NuGet Packages
  • Locate the Unity.Mvc package and install it

Once installed we need to locate the App_Start/UnityConfig.cs file and within the RegisterTypes method we add our mappings as usual, i.e.

container.RegisterType<IServerStatus, ServerStatus>();

There are also other IoC container NuGet packages including NInject (NInject.MVCx), with this we simply install the package relevent to our version of MVC, for example NInject.MVC4 and now we are supplied with the App_Start/NinjectWebCommon.cs file where we can use the RegisterServices method to register our mappings, i.e.

kernel.Bind<IServerStatus>().To<ServerStatus>();

More…

See Extending NerdDinner: Adding MEF and plugins to ASP.NET MVC for information on using MEF with ASP.NET.