Code-Sharp/WampSharp

[QUESTION] Is it possible to get the currently open channel in a Wamp Procedure Call?

shawty opened this issue · 16 comments

I'm on writing an application that uses a microservice architecture, and I'm using WampSharp for the various components to communicate with each other.

Right now I'm using the interface approach, and the interface files are shared between the various parts, for example in the client:

      channel = channelFactory.CreateJsonChannel(AuthServerWampEndpoint, "realm1");
      await channel.Open().ConfigureAwait(false);
      authProxy = channel.RealmProxy.Services.GetCalleeProxy<IAuthenticationService>();

      CreateUserDetails newUser = new CreateUserDetails()
      {
        FirstName = "A",
        LastName = "Person",
        Email = "a.person@example.com",
        MobileTelephoneNumber = "01234567890",
        SignInName = "person_a",
        Password = "mypassword"
      };

      await authProxy.CreateUser(newUser);

and in the service:

  public class AuthenticationService : IAuthenticationService
  {
    private readonly AuthDataContext _db;

    public AuthenticationService(AuthDataContext db)
    {
      _db = db;
    }

    public Task CreateUser(CreateUserDetails userDetails)
    {
      userDetails.Password = HashPassword(userDetails.Password);

        _db.LoadStoredProc("[dbo].[AddUser]")
          .AddParam("FirstName", userDetails.FirstName)
          .AddParam("LastName", userDetails.LastName)
          .AddParam("Email", userDetails.Email)
          .AddParam("MobileTelephoneNumber", userDetails.MobileTelephoneNumber)
          .AddParam("SignInName", userDetails.SignInName)
          .AddParam("PasswordHash", userDetails.Password)
          .ExecNonQuery();

      return Task.CompletedTask;
    }

  }

With everything being tied together using an interface as follows

  public interface IAuthenticationService
  {
    [WampProcedure("auth.createuser")]
    Task CreateUser(CreateUserDetails userDetails);
  }

NOTE: I'm just showing the minimum here to demo what I'm asking for.

This all works great, but I need to be able to send messages back to the calling module, NOT progress updates, but actual structured/published messages.

My thought on doing this was to attach a "subscriber" to the same channel I've created in the client in order to call the procedure, and that on the "receiving" end in the procedure I would be able to get the handle to that channel and then simply just "publish" to that same channel.

Attaching to the client is simple, I achieved that simply by doing the following

      var realmProxy = channel.RealmProxy;
      IDisposable subs =
        realmProxy.Services.GetSubject<string>("auth.api.notif")
          .Subscribe(x => {
            Console.WriteLine($"MESSAGE : {x}");
          });

Just after "opening" the channel, but before creating the procedure proxy and calling it.

I cannot however find a way to open the channel in my service call, so I can use it to "publish" on.

I need to be able to do this, as there are several steps that need to be taken when adding a user, such as checking to see if a name is used or not and informing the calling "User Interface" as this process, proceeds so the UI can update itself accordingly.

Is it possible to get the channel used to call into a wamp procedure?

If it's any help, everything here is being done using dotnet core 3.1, and kestrel services + blazor server, I'm currently using the following "middleware" to register my proxys in the server:

  public class WampMiddleware
  {
    private readonly RequestDelegate _next;
    private readonly ILogger<WampMiddleware> _logger;

    public WampMiddleware(RequestDelegate next, ILogger<WampMiddleware> logger, IWampHost wampHost, IServiceProvider provider)
    {
      _next = next;
      _logger = logger;

      var realm = wampHost.RealmContainer.GetRealmByName("realm1");

      realm.Services.RegisterCallee(() => provider.CreateScope().ServiceProvider.GetRequiredService<IAuthenticationService>());

    }

    public async Task Invoke(HttpContext httpContext)
    {
      _logger.LogInformation("WAMP RPC Call requested (Auth).");
      await _next(httpContext);
    }

  }

and registering it in startup like so:

      var wampHost = app.ApplicationServices.GetRequiredService<IWampHost>();
      app.Map("/ws", wsBuilder => {
        wsBuilder.UseWebSockets();
        wsBuilder.UseMiddleware<WampMiddleware>();

        wampHost.RegisterTransport(
          new AspNetCoreWebSocketTransport(wsBuilder),
          new JTokenJsonBinding());

      });
      wampHost.Open();

Thanks in advance for any pointers.

darkl commented

When creating the callee, you can pass a IWampSubject to its constructor so it can publish messages. Another option is to use RegisterPublisher with the callee (see Reflection based subscriber on the documentation website) and to raise your events from good old .NET event handlers.

Best
Elad

Thanks Elad, I'll take a look at that now.

When you say "RegisterPublisher" on the callee, would I register that on the middleware, in the startup class or in my service class?

darkl commented

At the same place where you call RegisterCallee (your middleware in your case).

Thanks Elad, testing now.. I'll let you know how I get on

Hi Elad, sorry for being a numpty, but what am I missing here?

Interface:
image

Service:
image

Registration (In Middleware) :
image

I can't create the publisher proxy as a normal "new" instance, as it's coming from the built in dotnet core DI container.

darkl commented

It does not make sense to register a publisher per call, because the way it works is that the framework registers to the publisher's events and publishes messages accordingly. This is something initiated by the publisher class and not by the client.

An alternative approach is to create a different class responsible for publications with the relevant event handlers, and send an instance of it to the RegisterPublisher method, i.e. call realm.Services.RegisterPublisher(ServiceProvider.GetRequiredService<IMyPublisher>()), where IMyPublisher is registered as a singleton. Make the constructor of the implementation of IAuthenticationService receive an instance of IMyPublisher and use that instance for publications.

Elad

I follow what your saying, but that may be a bit difficult in my case, because the client(caller) and the service(Callee) are in completely separate processes, may even be on different physical servers.

The only thing typing them together is a shared DLL containing nothing but the interfaces.

The "UI Part" is actually a "Blazor Server Side" application, which opens a new channel for each page accessed, and disposes the channel when a new page is navigated too. It's done this way as not all pages use the same services. Some use the services for customer access, some for employee etc etc

The authentication calls are in a 3rd process (I keep the auth stuff completely separate from the regular data API, again the ONLY thing the shared lib has in are interfaces, the concrete classes are all in the backend kestrel/web-api processes, and communication between UI parts and API parts are where wamp is being used.

Essentially, I'm using wamp as a simple message bus, or as an alternative to GRPC (Or trying to anyway) if that makes sense.

I'll continue pushing on though, take into mind your comments above see where I end up.

Actually, that singleton approach might work.... now that I think about it.

The concrete class is in the backend service, and the middleware is too.....

I'll get back to you on that one.

OK, so I guess I have to look down at my feet and shuffle away sheepishly :-D

I didn't think the singleton approach would work, but I could see how it might , but all I can say was I was proved wrong.

The correct approach in this case was as follows

Shared Lib with 2 interfaces in, one interface for the publisher, and one interface for the proc calls.

Auth service in a separate project, implementing 2 service classes, one that implements the publisher, and one that implements the proc calls. Publisher is registered in the auth service startup as a singleton, and the proc service as a scoped.

Both service classes are registered in the Auth service middleware via their respective interfaces.

The client in a totally separate process takes a reference only to the lib holding the interface, and attaches a subscriber to the topic registered in the publisher interface with receipt of the messages being handled inline using a Lambda (Currently just a stand alone console app used for testing, but eventually will be a blazor server UI)

Interfaces look as follows:
image

Service implementations like this:
image

Notification is used in proc call like this:
image

After first being injected into the service class using the DI container.

Middleware registers the proc & pub classes as follows:
image

And the interfaces/implementations are registered in startup like this:
image

That takes care of the service side.

In the client, we simply do something like this:
image

Just need a way to sync the topic strings between the two parts now and I'll be golden.

Thanks again for your help.

And I've synced the strings using the interface :-)

I think we can close this now, unless you have any other input.

darkl commented

Looks good. On the client side you can use Reflection-based subscriber for consistency (I guess that's what you meant when you said you synced the strings using the interface).

Elad

Hi Elad, nope I didn't I added a const to the interface file like so:
image

Then I changed the client to subscribe to the messages like this:
image

You got a pointer to the/a reflection sample? or a better way to do it, I'd love to hear, to my surprise however the above works really well.

PS: Little side question if I may.

Is it better to have your channels long running?

A session in a Blazor server is generally populated with scoped objects (Singletons in a Blazor WASM app) am I better off opening my channel in say a global scoped object when a request starts, and just attaching proxy's on the open channel in each page, or am I better opening and closing a new channel each time a page is requested (That's how I'm doing things at present)

darkl commented

Channels are designed for long runs.
You can find documentation for Reflection based subscriber on the documentation website.

Best
Elad