Calling Hub method from a controller's action in ASPNetCore MVC application using SignalR - asp.net-core-2.0

I would like the ASPNetCore2.0 webapp I'm working on to send a notification to specific users using SignalR. I would like to call the hub's method from another controller's action (as opposed to a client's JS call).
I have learned that this is not how SignalR is intended to be used, but I've found many users who had the same 'desire' and also some solutions.
I have checked several proposed solutions, but the simplest and cleaner seemed to be the accepted answer to this post: Get Hub Context in SignalR Core from within another object.
So I gave it a go, and I get no errors at all. The server's output is error-free, and so are the browser's console and network tabs (I'm using Chrome). When debugging, the flow is smooth and the program does exactly what it should do... except the users don't get any notification...
Do any of you spot the problem?
I created a class that contains the shared methods for the hub:
using Microsoft.AspNetCore.SignalR;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace WebApp.Hubs
{
public class HubMethods
{
private readonly IHubContext<PostsHub> _hubContext;
public HubMethods(IHubContext<PostsHub> hubContext)
{
_hubContext = hubContext;
}
public async Task Notify(string postId, string sender, List<string> users)
{
await _hubContext.Clients.Users(users).SendAsync("Notify", sender, postId);
}
}
}
Then I created a hub:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace WebApp.Hubs
{
[Authorize]
public class PostsHub : Hub
{
private HubMethods _hubMethods;
public PostsHub(HubMethods hubMethods)
{
_hubMethods = hubMethods;
}
public async Task Notify(string postId, string sender, List<string> users)
{
await _hubMethods.Notify(postId, sender, users);
}
}
}
Added these bits to Startup's ConfigureServices method:
[...]// Services before these...
services.AddSignalR();
services.AddScoped<HubMethods>();
services.AddMvc();
And Startup's Configure method:
app.UseSignalR(routes =>
{
routes.MapHub<PostsHub>("/postshub");
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Then these lines to the view:
<script src="~/lib/signalr/signalr.js"></script>
#await Html.PartialAsync("_NotifyScriptsPartial")
And this is "_NotifyScriptsPartial.cshtml":
<script>
var connection = new signalR.HubConnectionBuilder().withUrl('/PostsHub').build();
connection.on('Notify', function (sender, postId) {
var post = postId;
var sentBy = sender;
var content = '<a href=\'#\' class=\'close\' data-dismiss=\'alert\' aria-label=\'close\'>×</a>' +
'You just received a new comment from <strong>' +
sentBy + '</strong>. Click <a href = \'#\' class=\'alert-link\' >here</a> to view the post.'
var alert = document.createElement('div');
alert.classList.add('alert', 'alert-info', 'alert-dismissible');
alert.html(content);
document.getElementById('pageContent').appendChild(alert);
});
</script>
Finally, in the controller that is supposed to send the notification, I added these:
public class PostsController : Controller
{
private readonly HubMethods _hubMethods;
public PostsController(HubMethods hubMethods)
{
_hubMethods = hubMethods;
}
// POST: Create a new post
[Authorize]
[HttpPost]
public async Task<IActionResult> Create(DetailsModel model, List<string> readers)
{
if (ModelState.IsValid)
{
// Do stuff here... including creating the newPostId, userId and receipients variables used below
await _hubMethods.Notify(newPostId, userId, receipients);
// Do more stuff and eventually...
return View();
}
}
}
Any idea?

In Asp.Net Core 2.1 I can use hub like this, It solves my problem, also You used like this in your controller. Hope it helps.
public class SomeController : Controller
{
private readonly IHubContext<MyHub> _myHub;
public SomeController (IHubContext<MyHub> myHub)
{
_myHub = myHub;
}
public void SomeAction()
{
//for your example
_myHub.Clients.All.SendAsync("Notify", "data");
}
}
I can get the "data" text from browser's console. If you use jQuery in your project, add those codes between jQuery(document).ready(function () { }); because you tried to load a partial html and I think your code needs to run after ready() event. Sorry If I misunderstood you.

Related

Application hangs when sending from MVC Web Application to Azure IoT Hub Device

This is a controller code of an Azure Web App that handles a device notification logic. What I'm doing is invoking the code shown below from a ASP MVC Controller. But when I run it I get an ever-pending request from the browser(it hangs).
I have a button on view, when clicked, it invokes Wakeup method in the controller.
The code is not different from the one on MSDN for a console application. What am I missing?
using System.Text;
using Microsoft.Azure.Devices;
public class MyTemplate2Controller : Controller
{
static ServiceClient serviceClient;
static string connectionString = "HostName=azure-devices.net;SharedAccessKeyName=iothub;SharedAccessKey=mrUPt*2318C18K3LUk+oFarkNQ4vRvHrOa/eg=";
private AsyncController Task SendCloudToDeviceMessageAsync()
{
var asd = "{13A20041677B0,4,15,0}";
var commandMessage = new Message(Encoding.ASCII.GetBytes(asd));
return await serviceClient.SendAsync("Test_Comp_Dev_1", commandMessage).ConfigureAwait(continueOnCapturedContext: false);
}
public void Wakeup()
{
serviceClient = ServiceClient.CreateFromConnectionString(connectionString);
SendCloudToDeviceMessageAsync().Wait();
}
try the following:
public void Wakeup()
{
serviceClient = ServiceClient.CreateFromConnectionString(connectionString);
System.Threading.ThreadPool.QueueUserWorkItem(a => SendCloudToDeviceMessageAsync().Wait());
}

Do the Request filters get run from BasicAppHost?

I know that the services get wired-up by instantiating the BasicAppHost, and the IoC by using the ConfigureContainer property, but where is the right place to add the filters? The test in question never fire the global filter:
[TestFixture]
public class IntegrationTests
{
private readonly ServiceStackHost _appHost;
public IntegrationTests()
{
_appHost = new BasicAppHost(typeof(MyServices).Assembly)
{
ConfigureContainer = container =>
{
//
}
};
_appHost.Plugins.Add(new ValidationFeature());
_appHost.Config = new HostConfig { DebugMode = true };
_appHost.GlobalRequestFilters.Add(ITenantRequestFilter);
_appHost.Init();
}
private void ITenantRequestFilter(IRequest req, IResponse res, object dto)
{
var forTennant = dto as IForTenant;
if (forTennant != null)
RequestContext.Instance.Items.Add("TenantId", forTennant.TenantId);
}
[TestFixtureTearDown]
public void TestFixtureTearDown()
{
_appHost.Dispose();
}
[Test]
public void CanInvokeHelloServiceRequest()
{
var service = _appHost.Container.Resolve<MyServices>();
var response = (HelloResponse)service.Any(new Hello { Name = "World" });
Assert.That(response.Result, Is.EqualTo("Hello, World!"));
}
[Test]
public void CanInvokeFooServiceRequest()
{
var service = _appHost.Container.Resolve<MyServices>();
var lead = new Lead
{
TenantId = "200"
};
var response = service.Post(lead); //Does not fire filter.
}
}
ServiceStack is set at 4.0.40
Updated
After perusing the ServiceStack tests (which I highly recommend BTW) I came across a few example of the AppHost being used AND tested. It looks like the "ConfigureAppHost" property is the right place to configure the filters, e.g.
ConfigureAppHost = host =>
{
host.Plugins.Add(new ValidationFeature());
host.GlobalRequestFilters.Add(ITenantRequestFilter);
},
ConfigureContainer = container =>
{
}
Updated1
And they still don't fire.
Updated2
After a bit of trial and error I think it's safe to say that NO, the filters are not hooked up while using the BasicAppHost. What I have done to solve my problem was to switch these tests to use a class that inherits from AppSelfHostBase, and use the c# servicestack clients to invoke the methods on my service. THIS does cause the global filters to be executed.
Thank you,
Stephen
No the Request and Response filters only fire for Integration Tests where the HTTP Request is executed through the HTTP Request Pipeline. If you need to test the full request pipeline you'd need to use a Self-Hosting Integration test.
Calling a method on a Service just does that, i.e. it's literally just making a C# method call on a autowired Service - there's no intermediate proxy magic intercepting the call in between.

Specific TableController name not working

I have an extremely odd error and wondered if anyone knew the reason for this.
When I create a new DataObject and TableController called Content and ContentController respectively, it doesn't register the tablecontroller and the help documentation it automatically generates has lost its styling.
I can't connect to the controller at all but all other controllers work as expected.
If I just rename it to DataController and that's just the name of the controller, not the dataobject everything works perfectly.
Is ContentController a reserved word of some kind or is this just specifically happening on my machine?
public class DataController : TableController<Content>
{
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
MobileContext context = new MobileContext();
DomainManager = new EntityDomainManager<Content>(context, Request, Services);
}
// GET tables/Content
public IQueryable<Content> GetAllContent()
{
return Query();
}
// GET tables/Content/48D68C86-6EA6-4C25-AA33-223FC9A27959
public SingleResult<Content> GetContent(string id)
{
return Lookup(id);
}
// PATCH tables/Content/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task<Content> PatchContent(string id, Delta<Content> patch)
{
return UpdateAsync(id, patch);
}
// POST tables/Content/48D68C86-6EA6-4C25-AA33-223FC9A27959
public async Task<IHttpActionResult> PostContent(Content item)
{
Content current = await InsertAsync(item);
return CreatedAtRoute("Tables", new { id = current.Id }, current);
}
// DELETE tables/Content/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task DeleteContent(string id)
{
return DeleteAsync(id);
}
}
An MVC project will create an application directory called Content. This will override your route mapping to the ContentController.
You can get around this if desired through changing RouteMaps and other trickery although probably the simpliest answer is to change the name of the controller...

Using ServiceStack MiniProfiler to profile all service client calls

Context: I'm writing a service using ServiceStack. This service is calling some other remote services (using the ServiceStack JsonServiceClient).
Requirement: show every call to the remote service as a step in MiniProfiler.
Question: what would be the best way to implement this in a generic way?
The original code in my service looked like the following:
// Registration of the serviceclient in Apphost.cs:
// container.Register<IRestClient>(x => new JsonServiceClient("http://host:8080/"));
var client = ResolveService<IRestClient>();
HelloResponse response;
using (Profiler.Current.Step("RemoteService: Get Hello"))
{
response = client.Get(new Hello { Name = "World!" });
}
// ... do something with response ...
I wanted to get rid of the using (Profiler.Current.Step()) in this part of my code to make it easier to read and write.
// Registration of the serviceclient in Apphost.cs:
// container.Register<IRestClient>(x => new ProfiledRestClient("RemoteService", new JsonServiceClient("http://host:8080/")));
var client = ResolveService<IRestClient>();
HelloResponse response = client.Get(new Hello { Name = "World!" });
// ... do something with response ...
I made a wrapper around the existing client that contains the Profiler.Current.Step() code for every method of the IRestClient interface
mentioning the name of the client, the method and the request(type).
// The implementation of the wrapper:
public class ProfiledRestClient : IRestClient
{
readonly string clientName;
readonly IRestClient wrappedClient;
public ProfiledRestClient(string clientName, IRestClient wrappedClient)
{
this.clientName = clientName;
this.wrappedClient = wrappedClient;
}
public TResponse Get<TResponse>(IReturn<TResponse> request)
{
using (Profiler.Current.Step("{0}: Get {1}".Fmt(clientName, request.GetType().Name)))
{
return wrappedClient.Get(request);
}
}
public TResponse Post<TResponse>(IReturn<TResponse> request)
{
using (Profiler.Current.Step("{0}: Post {1}".Fmt(clientName, request.GetType().Name)))
{
return wrappedClient.Post(request);
}
}
// etc. the same for all other methods of IRestClient interface
}
It is working but it feels a bit dirty. Is there a better way of doing this?
Thank you for your insight.

WebApi Areas not found

I have a WebApi project and I am trying to add an area to it.
Is there something different that needs to be done when adding a new area to a webapi project vs a mvc4 application?
I have a simple area registration like
public class MobileAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Mobile";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Mobile_default",
"Mobile/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
A controller like
public class BusinessDetailsController : BaseController
{
public string Index()
{
return "hello world";
}
public HttpResponseMessage Get()
{
var data = new List<string> {"Store 1", "Store 2", "Store 3"};
return Request.CreateResponse(HttpStatusCode.OK, data);
}
}
However I can never reach the api. Am I doing something stupid or is there an extra step with the webapi that needs to be done?
Your code registers an MVC route for the Area, not a Web API route.
To do that use the MapHttpRoute extension method (you'll need to add a using statement for System.Web.Http).
public override void RegisterArea(AreaRegistrationContext context)
{
context.Routes.MapHttpRoute(
name: "AdminApi",
routeTemplate: "admin/api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
However, Areas are not really supported OOTB in ASP.NET Web API and you'll get an exception if you have two controllers with the same name (regardless of whether they are in different areas).
To support this scenario you need to change the way that controllers are selected. You'll find an article that covers how to do this here.

Resources