1

Trying to figure out a way to establish session management with DryIoC (v2.0.0-rc4build353), MS OWIN (v3.0.1, WebAPI2 (client v5.2.3) on VS2015, .Net 4.5.

I'm wrapping a rather elaborate legacy application with REST API. Strictly API server, no UI/MVC. I understand that it's impossible for me to go fully state-less because I have to keep a "model" open server-side. User must authenticate into the model as well. Hence concept of a Session came along. I want to use DI as much as possible.

My first discarded attempt was to use Ninject and map ISession to a provider factory. While Ninject has its pros (modules, for one), I didn't like the complexity of it. I can't figure out how to access request object from the factory. After some research I decided to switch to DryIoC.

In the code sample below DryIoC creates a singleton session (see Reuse below) and injects it into my RootController. If I register Session in Transient Scope, I obviously get a session per request. I envision that a call to, say, "api/login" will generate a token. Client will cache it and submit it with subsequent calls in a header (to enable API versioning as well).

Struggling with how to manage scoping.

EDIT: Clarification on what I think I need: I'm not sure how to implement a factory that DryIoC would call before instantiating a controller, where I'd lookup the session token and create/lookup associated ISession instance. DryIoC would then use it to inject into the controller.

EDIT: I am trying to hide all session management boilerplate and have all controllers be injected with an already initialized session. In case there's no session for this request, a separate route would return an error. Another thing to note is that a client has to acquire a token explicitly. There's no notion of a global "current" token or session.

using System;
using System.Web.Http;

using Microsoft.Owin.Hosting;
using Microsoft.Owin.Diagnostics;

using Owin;

using DryIoc;
using DryIoc.WebApi;

namespace di_test
{
    class Program
    {
        static void Main(string[] args)
        {
            var url = "http://localhost:8065";

            using (WebApp.Start<Startup>(url))
            {
                Console.WriteLine("Owin host started, any key to exit");
                Console.ReadKey();
            }
        }
    }

    class Startup
    {
        public void Configuration(IAppBuilder app_)
        {
            var config = new HttpConfiguration();
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "default",
                routeTemplate: "{controller}"
               );

            var di = new DryIoc.Container();
            di.Register<ISession, Session>(Reuse.Singleton);
            di.WithWebApi(config);

            app_.UseWebApi(config);
            app_.UseErrorPage(ErrorPageOptions.ShowAll);
        }
    }

    public interface ISession
    {
        string Token { get; }
    }

    public class Session : ISession
    {
        string m_token = null;

        public Session()
        {
            Console.WriteLine("Session()");
        }

        public string Token => m_token ?? (m_token = Guid.NewGuid().ToString());
    }

    [RoutePrefix("api")]
    public class RootController : ApiController
    {
        readonly ISession m_session;

        public RootController(ISession session_)
        {
            m_session = session_;
        }

        [Route()]
        public IHttpActionResult GetApiRoot()
        {
            return Json(
                new
                {
                    type = "root",
                    token = m_session.Token
                });
        }
    }
}
Yuriy Gettya
  • 693
  • 10
  • 20
  • I don't quite understand: you want to have a session _per what_? – Fyodor Soikin Nov 19 '15 at 09:54
  • @FyodorSoikin Session per "something" :) I'm not sure how to implement a factory that DryIoC would call before instantiating a controller, where I'd lookup the session token and create/lookup associated ISession instance. DryIoC would then use it to inject into the controller. – Yuriy Gettya Nov 19 '15 at 11:34
  • I think you are over complicating this. If the legacy system has the ability to call an API then it has the ability to call an API for a token. The token gets stored on the client side and passed in the header when making a call to the resource API. Look up JWT and oAuth. An API should never have the concept of a session. – Stephen Brickner Nov 19 '15 at 21:43
  • @StephenBrickner This is only the "server side" of a larger system. There are other modules (Node/Angular) that will call in. JWT/oAuth is in the plans and exactly what I meant by the "pass token in header", however the problem is that I want to instantiate the controller with correct session injected into it. In order to inject the correct session instance, I need DI container to call me with an http context, which I can't figure out how to do. – Yuriy Gettya Nov 20 '15 at 01:07

2 Answers2

3

First, I am not a Web guy (but DryIoc maintainer), and did not get fully what session management do you want to achieve. But if you want to utilize request for that, you can do that as following:

public class Startup
{
    public void Configuration(IAppBuilder app_)
    {
        var config = new HttpConfiguration();
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "default",
            routeTemplate: "{controller}"
           );

        var di = new DryIoc.Container();

        // NOTE: Registers ISession provider to work with injected Request
        di.Register<ISession>(Made.Of(() => GetSession(Arg.Of<HttpRequestMessage>())));

        di.WithWebApi(config);

        app_.UseWebApi(config);
        app_.UseErrorPage(ErrorPageOptions.ShowAll);
    }

    public static ISession GetSession(HttpRequestMessage request)
    {
        // TODO: This is just a sample. Insert whatever session management logic you need.
        var session = new Session();
        return session;
    }
}

DryIoc will inject current HttpRequestMessage into GetSession. Then GetSession will be used to inject result session into controller.

Here is more details how to use DryIoc factory methods.

BTW I have pushed this sample app into DryIoc dev branch as a DryIoc.WebApi.Owin.Sample app. I hope you are OK with it. Poke me if you are not.

dadhi
  • 4,807
  • 19
  • 25
  • Re: sample app - perfectly fine. Re: inject HttpRequestMessage, that's what I was missing! Thank you, I'll experiment with this and comment. For now this would be the answer. I appreciate you looking at it! – Yuriy Gettya Nov 20 '15 at 13:46
0

If you only want to do this once for this one particular interface, and you don't mind managing its lifetime manually, you can just literally create a factory using RegisterDelegate:

var sessions = new ConcurrentDictionary<Token, Session>();

di.RegisterDelegate<ISession>( _ => {
    var token = GetCurrentToken();
    return sessions.GetOrAdd( token, t => new Session( t ) );
} );

Or, if you want to have this idea of "per token" apply to a wide range of components, you can implement a custom IReuse, which is a thing that can look up "current scope", and "scope" is just a cache for instances.

However, I would not recommend this design. This is the wrong layer for this kind of logic.

One example: what do you do when there is no current token? You can throw an exception of course, but there is nowhere to catch that exception - you're still just constructing your component graph at that point, - so the client will just see whatever WebApi autogenerated exception message is. Ugly. Not to mention that you don't get to do any recovery, log the incident, and so on, and so forth.

Another example: at a later stage in your development, you will very likely want to do something with that token before constructing the session. Say, validate it, or check if it's expired or has been recalled, or whatever. And now you're adding higher-level business logic to your DI wiring code. Ugly. Unmaintainable.

A better approach would be to have a singleton component called ISessionManager, then have it injected into your controller, and have the controller call ISessionManager.GetCurrentSession() or something. Then you're not tying yourself down: later you can choose to extend what GetCurrentSession does, or you can choose to change its return type to include possible errors (e.g. "no token", "token expired", etc.), and have the controller report that back to the consumer.

Think about it.

Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
  • I have considered a session manager approach and even wrote some test code. The problem (as I understand it, I might be wrong) still lies with: - Mapping HTTP Header to a Session if any - Injecting an instance of mapped session into all controllers. So should this be a job for custom middleware? – Yuriy Gettya Nov 19 '15 at 15:11
  • Don't inject instance of session. Inject session manager itself. Then have the controller call a method on that instance to obtain current session. – Fyodor Soikin Nov 19 '15 at 15:14
  • Right, but there can be more than one session in flight. Also, how would session manager get access to current request? .. Oh I see, you're proposing that every REST-endpoint would have some boilerplate code at the start - "lookup a session or quit". Sigh I was trying to hide it and have the controller oblivious to the "how" of session management. We're talking about a number of controllers – Yuriy Gettya Nov 19 '15 at 15:17
  • No, you don't have to have the "boilerplate code", if you don't want to. The session manager can get access to the current request using `HttpContext.Current` (bad option) or have `HttpContextBase` injected through DI (for which you'll have to use `RegisterDelegate` with `Reuse.Transient`). However, with the way MVC works, you pretty much _have to_ have boilerplate code if you want your application to be well-designed. You can do with a generic filter that just returns an error when there is no session, but that is still very inflexible. – Fyodor Soikin Nov 19 '15 at 15:21
  • This is not an MVC app, plain OWIN/WebApi2, so no HttpContext (AFAIK). To reiterate - strictly an API server. My gut feeling is that having maybe having a middleware filter is the right approach? I don't understand enough of DryIoC or WebAPI2 internals to formulate a solution. – Yuriy Gettya Nov 19 '15 at 15:23
  • 1
    Oh, right, missed that. You can still inject `HttpRequestMessage` into session manager. DryIoc should take care of retrieving the current one for you (though I never tried myself). – Fyodor Soikin Nov 20 '15 at 13:17