Search code examples
javascriptc#asp.net-web-apisignalrsystemjs

Unable to make SignalR, Web API, and SystemJS work together


I am trying to build a notification service on my Web API that notifies the JavaScript (Aurelia) clients (WebApp). My Web API, and WebApp are in different domains.

I have a simple NotificationHub:

public class NotificationHub:Hub
{
    public void NotifyChange(string[] values)
    {
        Clients.All.broadcastChange(values);
    }
}

I am configuring SignalR in Startup of Web API as follows:

public void Configuration(IAppBuilder app)
{
    HttpConfiguration httpConfig = new HttpConfiguration();

    var cors = new EnableCorsAttribute("http://localhost:9000", "*", "*");
    httpConfig.EnableCors(cors);

    app.MapSignalR();

    WebApiConfig.Register(httpConfig);
    ...
}

In my WebApp, I am trying to access the signalr/hubs from my Web API as follows:

Promise.all([...
        System.import('jquery'),
        System.import('signalr'), ...
            ])
       .then((imports) => {
            return System.import("https://localhost:44304/signalr/hubs");
            });

I have also added a meta section in my config.js:

System.config({
    ...
    meta: {
        "https://*/signalr/hubs": { //here I also tried with full url
            "format": "global",
            "defaultExtension": false,
            "defaultJSExtension": false,
            "deps": ["signalr"]
        }
    }
});

Despite of these configurations, I still have following problems:

  1. The request for signalr/hubs, from WebApp, is made as https://localhost:44304/signalr/hubs.js, and a HTTP 404 is returned. Note that browsing https://localhost:44304/signalr/hubs returns the hubs script.
  2. On $.connection("https://localhost:44304/signalr").start(), I am receiving following error:

XMLHttpRequest cannot load https://localhost:44304/signalr/negotiate?clientProtocol=1.5&_=1471505254387. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:9000' is therefore not allowed access.

Please let me know what I am missing here?

Update: Using the appropriate CORS configuration, and the gist suggested by @kabaehr I am now able to connect to SignalR hubs. However, the broadcast (push notification) is still not working.

My SignalR configuration:

public static class SignalRConfig
{
    public static void Register(IAppBuilder app, EnableCorsAttribute cors)
    {

        app.Map("/signalr", map =>
        {
            var corsOption = new CorsOptions
            {
                PolicyProvider = new CorsPolicyProvider
                {
                    PolicyResolver = context =>
                    {
                        var policy = new CorsPolicy { AllowAnyHeader = true, AllowAnyMethod = true, SupportsCredentials = true };

                        // Only allow CORS requests from the trusted domains.
                        cors.Origins.ForEach(o => policy.Origins.Add(o));

                        return Task.FromResult(policy);
                    }
                }
            };
            map.UseCors(corsOption).RunSignalR();
        });
    }
}

I am using it in Startup as follows:

var cors = new EnableCorsAttribute("http://localhost:9000", "*", "*");
httpConfig.EnableCors(cors);

SignalRConfig.Register(app, cors);

WebApiConfig.Register(httpConfig);

And I am trying to push notifications as follows:

GlobalHost.ConnectionManager.GetHubContext<NotificationHub>().Clients.All.broadcastChange(new[] { value });

However, my clients are not notified on broadcastChange. I am assuming that I don't have to import https://localhost:44304/signalr/hubs explicitly, when I am using the gist.


Solution

  • This is just to share my findings and what solved the problem for me.

    So to start with, signalr/hubs is just a auto generated proxy from the server side code. It is not necessary to use that proxy if you can create your own SignalR proxy client. Below is one such simple SignalR client that is created based on the gist mentioned by @kabaehr. The SignalR client is quite simplistic in nature as of now.

    export class SignalRClient {
    
        public connection = undefined;
        private running: boolean = false;
    
        public getOrCreateHub(hubName: string) {
            hubName = hubName.toLowerCase();
            if (!this.connection) {
                this.connection = jQuery.hubConnection("https://myhost:myport");
            }
    
            if (!this.connection.proxies[hubName]) {
                this.connection.createHubProxy(hubName);
            }
    
            return this.connection.proxies[hubName];
        }
    
        public registerCallback(hubName: string, methodName: string, callback: (...msg: any[]) => void,
            startIfNotStarted: boolean = true) {
    
            var hubProxy = this.getOrCreateHub(hubName);
            hubProxy.on(methodName, callback);
    
            //Note: Unlike C# clients, for JavaScript clients, at least one callback 
            //      needs to be registered, prior to start the connection.
            if (!this.running && startIfNotStarted)
                this.start();
        }
    
        start() {
            const self = this;
            if (!self.running) {
                self.connection.start()
                    .done(function () {
                        console.log('Now connected, connection Id=' + self.connection.id);
                        self.running = true;
                    })
                    .fail(function () {
                        console.log('Could not connect');
                    });
            }
        }
    }
    

    One important thing to note here is that for a JavaScript SignalR client, we need to register at least one callback method before starting the connection.

    With such client proxy in place you can use it as below. Though the code sample below uses aurelia-framework in general, but the SignalR part in attached() has nothing to do with Aurelia.

    import {autoinject, bindable} from "aurelia-framework";
    import {SignalRClient} from "./SignalRClient";
    
    @autoinject
    export class SomeClass{
    
        //Instantiate SignalRClient.
        constructor(private signalRClient: SignalRClient) {
        }
    
        attached() {
            //To register callback you can use lambda expression...
            this.signalRClient.registerCallback("notificationHub", "somethingChanged", (data) => {
                console.log("Notified in VM via signalr.", data);
            });
    
            //... or function name.
            this.signalRClient.registerCallback("notificationHub", "somethingChanged", this.somethingChanged);
        }
    
        somethingChanged(data) {
            console.log("Notified in VM, somethingChanged, via signalr.", data);
        }
    }
    

    This is the crux of the solution. For the parts associated with enabling CORS is already mentioned in the question. For more detailed information, you may refer to these links: