Search code examples
c#asp.net-web-api.net-4.5server-sent-events

Forward Server-Sent-Events from another service to the caller - C# ASP.NET WebApi


I have a web app made with ASP.NET (constrained to .NET 4.5) using the WebApi which has a controller class.
In this controller, I had to make two new endpoints who have to call another service which is instead a Java application using Spring Web, but that might be besides the point.
The problem here is that, one of these two endpoints needs to simply forward Server-Sent-Events sent by the spring application.

I have no idea how to do this in c#. These are the two things I've tried so far but obviously they didn't work:

[HttpPost]
[Route("api/myApp/myEndpoint")]
public async Task<HttpResponseMessage> myEndpoint([FromBody] MyEndpointParams params)
{
    try
    {
        using (var client = new HttpClient(new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
        }))
        {
            string uri = "https://theServer/theSpringApp/api/theServerEventsSendingEndpoint";
            StringContent content = new StringContent(JsonConvert.SerializeObject(params), Encoding.UTF8, "application/json");
            HttpRequestMessage request = new HttpRequestMessage()
            {
                Method = HttpMethod.Post,
                RequestUri = new Uri(uri),
                Content = content
            };

            return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
        }
    }
    catch (Exception ex)
    {
        log.Error("[myEndpoint] Error", ex);
        return new HttpResponseMessage(HttpStatusCode.InternalServerError);
    }
}
[HttpPost]
[Route("api/myApp/myEndpoint")]
public async Task<IDisposable> myEndpoint([FromBody] MyEndpointParams params)
{
    try
    {
        using (var client = new HttpClient(new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
        }))
        {
            string uri = "https://theServer/theSpringApp/api/theServerEventsSendingEndpoint";
            StringContent content = new StringContent(JsonConvert.SerializeObject(params), Encoding.UTF8, "application/json");
            HttpRequestMessage request = new HttpRequestMessage()
            {
                Method = HttpMethod.Post,
                RequestUri = new Uri(uri),
                Content = content
            };

            var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
            return await response.Result.Content.ReadAsStreamAsync();
        }
    }
    catch (Exception ex)
    {
        log.Error("[myEndpoint] Error", ex);
        return new HttpResponseMessage(HttpStatusCode.InternalServerError);
    }
}

I wish there was a way to simply forward the input of the http call I need to make to the output of my response...

What I actually need to do: the Spring application needs to stream text as it completes a certain operation. I need to read that text in real time to show something on the user's screen as that operation is going on.
I know that I could make an asynchronous operation that stores its state somewhere and make a call like every second to fetch the progress, but I really wanted to try doing it this way instead.



In case you need to know better what goes on in the Spring service, this is part of the code its endpoint uses (I've changed the names of stuff, of course):
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

...

private ExecutorService executorService = Executors.newCachedThreadPool();

...

@PostMapping(path = "/api/theServerEventsSendingEndpoint", produces = MediaType.TEXT_PLAIN_VALUE)
public SseEmitter sendTheServerEvents(@RequestBody(required = true) MyEndpointParams params) {

    SseEmitter emitter = new SseEmitter();
    executorService.execute(() -> {
        // does stuff in a loop, and in every loop iteration it does:
        emitter.send("some text that depends on the operation it's doing");

        // and once it ends:
        emitter.complete();
            
        // (I've omitted the code for error management of course)
    });
    return emitter;
}

Thank you for any help.

Solution

  • If you don't process the response from the Java side and just need to return it to the client I would not use C#. I would use the URL Rewrite Module and ARR if you are running under IIS or a reverse proxy under apache2.

    I don't know much about apache2 but under IIS you can use this:

    https://learn.microsoft.com/en-us/iis/extensions/url-rewrite-module/reverse-proxy-with-url-rewrite-v2-and-application-request-routing

    https://learn.microsoft.com/en-us/iis/extensions/url-rewrite-module/using-the-url-rewrite-module

    If you need to process the response before sending it to the client and you can also control the browser code I would use WebSockets or SignalR. Especially if you really get something like "Events" or a long running response from the Java side.

    https://learn.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-7.0

    If you want to stay with api and C# I think you can not use Controllers. You have to switch to handlers or in ASP.net Core handle the request on the path directly. As soon as you have access to the response object, you can write the result of your Java call to the response stream.

    In (pseudo) C# and Asp.Net Core 7 it would look like:

        app.MapGet("/your-api-url", async (HttpContext context) => {
            //Call Java side with a Request or HttpClient
            //while get responseFromJava
               //optional responseFromJavaProcessed = process(responsFromJava)
               await context.Response.WriteAsync(responseFromJavaProcessed);
      
            //After Loop - just exit here
        });