4

In an app running Framework 4.72, not .NET Core, I'm trying to inject a SignalR IHubContext into a Web API 2.x service. I have my solution broken into three projects, web, service, data. The SignalR hub is in the web layer. I have background code that runs in the service layer and when complete I need it to send a mesage via the hub. This background task is not initiated by the controller.

My Global.asax is pretty standard:

 protected void Application_Start()
{
    GlobalConfiguration.Configure(WebApiConfig.Register);

    // Set JSON serializer to use camelCase
    var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    json.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

    DIConfig.Setup();

    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

    var logConfigFilePath = Server.MapPath("~/log4net.config");
    log4net.Config.XmlConfigurator.ConfigureAndWatch(new System.IO.FileInfo(logConfigFilePath));
}

My DIConfig contains:

internal static void Setup()
{
    var config = System.Web.Http.GlobalConfiguration.Configuration;
    var builder = new ContainerBuilder();

    builder.Register(c => new ShopAPDbContext()).AsImplementedInterfaces().InstancePerBackgroundJob().InstancePerLifetimeScope();
    builder.RegisterType<ShopAPRepository>().As<IShopAPRepository>().InstancePerBackgroundJob().InstancePerLifetimeScope();
    builder.RegisterType<ShopAPService>().As<IShopAPService>().InstancePerBackgroundJob().InstancePerLifetimeScope();

    builder.AddAutoMapper(typeof(InvoiceMappingProfile).Assembly);
    builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
    var container = builder.Build();

    DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    config.DependencyResolver = new AutofacWebApiDependencyResolver(container);

    Hangfire.GlobalConfiguration.Configuration.UseAutofacActivator(container);
}

And my Startup.cs:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var container = DependencyConfiguration.Configure(app);
        SignalRConfiguration.Configure(app, container);
        HangFireDashboardConfig.Configure(app);
    }
}

public static class DependencyConfiguration
{
    public static IContainer Configure(IAppBuilder app)
    {
        var builder = new ContainerBuilder();
        builder.RegisterHubs(typeof(SignalRConfiguration).Assembly);
        var container = builder.Build();
        app.UseAutofacMiddleware(container);
        return container;
    }
}

public static class SignalRConfiguration
{
    public static void Configure(IAppBuilder app, IContainer container)
    {
        HubConfiguration config = new HubConfiguration();
        config.Resolver = new AutofacDependencyResolver(container);

        app.Map("/messages", map =>
        {
            map.UseCors(CorsOptions.AllowAll);
            var hubConfiguration = new HubConfiguration
            {
                EnableDetailedErrors = true,
                EnableJavaScriptProxies = false
            };
            map.RunSignalR(hubConfiguration);
        });
    }
}

The constructors of my service layer look like this:

public ShopAPService()
{
    _shopAPRepository = new ShopAPRepository();
    _mapper = new Mapper((IConfigurationProvider)typeof(InvoiceMappingProfile).Assembly);
    _hubContext = null;  // what here?
}

public ShopAPService(IShopAPRepository shopAPRepository, IMapper mapper, IHubContext hubContext)
{
    _shopAPRepository = shopAPRepository;
    _mapper = mapper;
    _hubContext = hubContext;
}

I know I need to pass an instance of the IHubContext into the service but so far I haven't been succesful. As I know, the first constructor is what is used when the service is called from anything other than the controller?


FIRST REVISION

Ok, I understand that everything should go into a single container. Based on feedback and looking at those links, I create a container and pass that along. Here is my revised Startup:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        //var config = System.Web.Http.GlobalConfiguration.Configuration;
        var container = GetDependencyContainer();

        RegisterWebApi(app, container);
        RegisterSignalR(app, container);

        GlobalHost.DependencyResolver = new AutofacDependencyResolver(container);
        HubConfiguration config = new HubConfiguration();
        config.Resolver = new AutofacDependencyResolver(container);

        //config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
        Hangfire.GlobalConfiguration.Configuration.UseAutofacActivator(container);

        HangFireDashboardConfig.Configure(app);
    }

    private IContainer GetDependencyContainer()
    {
        return AutofacConfig.RegisterModules();
    }

    private void RegisterWebApi(IAppBuilder app, IContainer container)
    {
        var configuration = new HttpConfiguration
        {
            DependencyResolver = new AutofacWebApiDependencyResolver(container)
        };

        WebApiConfig.Register(configuration);

        app.UseAutofacMiddleware(container);
        app.UseAutofacWebApi(configuration);
        app.UseWebApi(configuration);
    }

    private void RegisterSignalR(IAppBuilder app, IContainer container)
    {
        var configuration = new HubConfiguration
        {
            Resolver = new AutofacDependencyResolver(container)
        };

        app.MapSignalR(configuration);
    }
}

And my AutofacConfig builds the container and does most of the registering:

internal class AutofacConfig
{
    public static IContainer RegisterModules()
    {
        var builder = new ContainerBuilder();

        builder.Register(c => new ShopAPDbContext()).AsImplementedInterfaces().InstancePerBackgroundJob().InstancePerLifetimeScope();
        builder.RegisterType<ShopAPRepository>().As<IShopAPRepository>().InstancePerBackgroundJob().InstancePerLifetimeScope();
        builder.RegisterType<ShopAPService>().As<IShopAPService>().InstancePerBackgroundJob().InstancePerLifetimeScope();

        builder.RegisterAutoMapper(typeof(InvoiceMappingProfile).Assembly);
        builder.RegisterApiControllers(Assembly.GetExecutingAssembly());

        // Register Autofac resolver into container to be set into HubConfiguration later
        builder.RegisterType<AutofacDependencyResolver>().As<IDependencyResolver>().SingleInstance();

        // Register ConnectionManager as IConnectionManager so that you can get hub context via IConnectionManager injected to your service
        builder.RegisterType<ConnectionManager>().As<IConnectionManager>().SingleInstance();

        builder.RegisterHubs(Assembly.GetExecutingAssembly());

        var container = builder.Build();

        return container;
    }
}

But, I still am not able to get the reference to the Hub in my Service (which is in a seperate project than the API but in the same solution.

solutioin screenshot

My Service method is called from HangFire and has no reference to the Hub. How can I reference it in my parameterless constructor?

public partial class ShopAPService : IShopAPService
{
    private static readonly ILog _log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

    readonly IShopAPRepository _shopAPRepository;
    readonly IMapper _mapper;
    private IHubContext _hubContext;

    public ShopAPService()
    {
        _shopAPRepository = new ShopAPRepository();
        _mapper = new Mapper((IConfigurationProvider)typeof(InvoiceMappingProfile).Assembly);
        _hubContext = connectionManager.GetHubContext<MessageHub>();
    }

    public ShopAPService(IShopAPRepository shopAPRepository, IMapper mapper, IHubContext hubContext)
    {
        _shopAPRepository = shopAPRepository;
        _mapper = mapper;
        _hubContext = hubContext;
    }
}
Connie DeCinko
  • 802
  • 2
  • 15
  • 32

1 Answers1

2

You can't resolve IHubContext directly. But you can resolve generic implementation of this interface. More details described here and here.

I just create very simple implementation of OWIN (Stratup.cs). I have installed Autofac.SignalR nugget package and used method RegisterHubs for registration and method MapSignalR for mapping. It is standard approach and it is enghoy to resolve typed Hub implementation.

But if you wish resolve context more correct then you need to add two more registrations: AutofacDependencyResolver and ConnectionManager (more info available here).

Please review full sample:

using Autofac;
using Autofac.Integration.SignalR;
using Autofac.Integration.WebApi;
using Microsoft.AspNet.SignalR;
using Microsoft.Owin;
using Owin;
using System.Reflection;
using System.Web.Http;

[assembly: OwinStartup(typeof(Startup))]
namespace Sample
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var container = GetDependencyContainer();

            RegisterWebApi(app, container);
            RegisterSignalR(app, container);
        }

        private IContainer GetDependencyContainer()
        {
            var builder = new ContainerBuilder();

            AutofacConfig.RegisterModules(builder);
            builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
            builder.RegisterHubs(Assembly.GetExecutingAssembly());

            // Register Autofac resolver into container to be set into HubConfiguration later
            builder.RegisterType<AutofacDependencyResolver>().As<IDependencyResolver>().SingleInstance();
            // Register ConnectionManager as IConnectionManager so that you can get hub context via IConnectionManager injected to your service
            builder.RegisterType<ConnectionManager>().As<IConnectionManager>().SingleInstance();

            var container = builder.Build();

            return container;
        }

        private void RegisterWebApi(IAppBuilder app, IContainer container)
        {
            var configuration = new HttpConfiguration
            {
                DependencyResolver = new AutofacWebApiDependencyResolver(container)
            };

            WebApiConfig.Register(configuration);

            app.UseAutofacMiddleware(container);
            app.UseAutofacWebApi(configuration);
            app.UseWebApi(configuration);
        }

        private void RegisterSignalR(IAppBuilder app, IContainer container)
        {
            var configuration = new HubConfiguration
            {
                Resolver = new AutofacDependencyResolver(container)
            };

            app.MapSignalR(configuration);
        }
    }
}

My Hub is standard and simple:

public class MaintenanceHub : Hub
{
    public MaintenanceHub(IMaintenanceLogProvider maintenanceLogProvider)
    {
        maintenanceLogProvider.TaskProgressStatusEvent += (s, e) => GetTaskLogStatus(e);
    }

    public void GetTaskLogStatus(LongTaskProgressStatus taskProgressStatus)
    {
        Clients.All.getTaskLogStatus(taskProgressStatus);
    }
}

After registration you can use Hub inject context. Something like it (available 2 options, you can use only one what is you wish):

public AccountController(IConnectionManager connectionManager, MaintenanceHub maintenanceHub)
{
    var context = connectionManager.GetHubContext<MaintenanceHub>();
    var hub = maintenanceHub;
}

enter image description here

Alexander I.
  • 2,380
  • 3
  • 17
  • 42
  • Where is AutofacConfig? – Connie DeCinko Dec 03 '20 at 20:43
  • @alexandar How do I inject the hub into a service that is in another class? – Connie DeCinko Dec 03 '20 at 22:28
  • @ConnieDeCinko about AutofacConfig: it is just my business logic, that register Autofac modules (Database module, Infrastructure module, etc). This class is not interesting for you. You can just remove it. – Alexander I. Dec 04 '20 at 06:51
  • @ConnieDeCinko Please review StartUp class. There contains registration for **AutofacDependencyResolver** and **ConnectionManager**. You can inject **IConnectionManager** to your service classes. You can extract Hub Context from **IConnectionManager** something like it: `var context = connectionManager.GetHubContext();` – Alexander I. Dec 04 '20 at 06:54
  • I've updated my code but I still am not able to reference the Hub inside my service that is called without any parameters. The method inside is called when HangFire completes a background task. – Connie DeCinko Dec 04 '20 at 18:31
  • @ConnieDeCinko I see you have added registration. It is correct. But you try to inject **HubContext** incorrect. You need to inject **IConnectionManager** inside your `ShopAPService` constructor. And why you try to use parameter-less constructor? You need to provide all necessary paramters to the constructor and Autofac must inject interfaces for you. – Alexander I. Dec 06 '20 at 10:01
  • In your controller example above you call var context = connectionManager.GetHubContext(); which is fine as your controller and hub are in the sampe project. But my service is in another project. I cannot add a reference to the project where the hub is as that will create a circular reference. Do I move my hubs into the service project? – Connie DeCinko Dec 07 '20 at 16:34
  • @ConnieDeCinko I understand your trouble. Not sure move hubs to the service project it is a good architecture idea. For me good architecture when your API and SignalR projects referenced to the Service project (as you implemented for now). I think you can try to implement event or delegate approach. – Alexander I. Dec 07 '20 at 17:38
  • And new information that you provided - it a separate question. If you will implement events or delegates (Action<> or Func<>) then no need to register and inject HubContext. So with this realization you registration will be simple. I think provide Action<> parameter to your Service method it is a best option. If you wish you can create new one question and I will answer you and describe my idea more detailed (very hard to explain idea inside SO comment) – Alexander I. Dec 07 '20 at 17:50
  • Simplified question here: https://stackoverflow.com/questions/65187645/send-messages-via-signalr-hubcontext-from-method-in-project-outside-where-hubs-a – Connie DeCinko Dec 07 '20 at 18:49