What did I get wrong, DI or Design, and how should

2019-08-28 06:25发布

问题:

To make a long story short, the application that I am currently writing ought to impersonate the current logged in user.

It's an application to manage information inquiries.

Because of NHibernate.ISessionFactory not allowing more flexibility at the level of its connection string, I need to build the connection dynamically using the current user credentials. (By the way, I'm not complaining against NH, it's a wonderful that I use on each project.)

So, I need to force authentication at start up. Many dependencies may be bound on startup, but not those of the data access module, which require to be loaded up after the user has authenticated because of the connection string.

  1. Authentication if forced;
  2. The AuthenticationPresenter tells the MembershipService to authenticate the user using the provided credentials;
  3. On successful authentication, an instance of the AuthetnicationUser class is kept within the MembershipService, which in turn is bound in singleton scope;
  4. The SessionFactoryProvider depends on my MembershipService.CurrentUser property to retrieve the credentials used for building the connection string, thus the CurrentUser property cannot be null on this module's loading, hence I can't load this module on application startup without breaking something.

It is a Windows Form application, so I use the Program class to instantiate the Ninject IKernel and load the modules required for application startup.

The application later requires for new dependencies to be resolved such the data access, the inquiries management and the repositories which depends on a ISession.

The solution should be to be able to either load the modules that require the CurrentUser to be known later in the application lifecycle, or perhaps using contextual or conditional dependency injection binding.

DataModule

public class DataModule : NinjectModule {
    public override void Load() {
        Bind<ISessionFactory>().ToProvider<SessionFactoryProvider>().InSingletonScope();
        Bind<ISession>().ToProvider<SessionProvider>();
        Bind<IStatelessSession>().ToProvider<StatelessSessionProvider>();
    }
}

ServiceModule

public class ServiceModule : NinjectModule {
    public override void Load() {
        Bind<IMembershipService>().To<MembershipService>().InSingletonScope();
    }
}

AuthenticationModule

public class AuthenticationModule : NinjectModule {
    public override void Load() {
        Bind<AuthenticationPresenter>().ToSelf().InSingletonScope();
        Bind<IAuthenticationPresenterFactory>().ToFactory();
        Bind<IAuthenticationView>().To<AuthenticationForm>();
    }
}

InquiriesManagementModule

public class InquiriesManagementModule : NinjectModule {
    public override void Load() {
        Bind<IInquiriesManagementPresenterFactory>().ToFactory();
        Bind<InquiriesManagementPresenter>().ToSelf().InSingletonScope();
        Bind<IInquiriesManagementView>().To<InquiriesMgmtForm>();
        Bind<ICancelInquiryPresenterFactory>().ToFactory();
        Bind<CancelInquiryPresenter>().ToSelf();
        Bind<ICancelInquiryView>().To<CancelInquiryForm>();
        Bind<IEditInquiryPresenterFactory>().ToFactory();
        Bind<EditInquiryPresenter>().ToSelf();
        Bind<IEditInquiryView>().To<EditInquiryForm>();
        Bind<INewInquiryPresenterFactory>().ToFactory();
        Bind<NewInquiryPresenter>().ToSelf();
        Bind<INewInquiryView>().To<NewInquiryForm>();
        Bind<IInquiriesRepository>().To<InquiriesRepository>();
    }
}

Should you require further details, please have a look at this question:

Conditional dependency injection binding only when property not null

I have also thought of writing an ApplicationContext class that could contain such essential information as the current user to pass it along the application's presenters and provider that require it, and still, the problem remains.

So I wonder whether it is a design flaw or a lack of knowledge of the dependency injection tool, or maybe both.

EDIT

Following @Simon Whitehead's comment,

Have you thought about using the WithConstructorArgument method to pass in user credentials?

I have not thought of using WithConstructorArgument for a couple of reasons:

  1. I'm using Ninject for the second time in my life
  2. I do think that I have to provide values to these arguments, which I don't yet have upon application startup, because no user has been authenticated yet
  3. The dependencies are loaded upfront application startup, so it throws even before getting to the authentication window, which is the first and only thing the user can do when double-clicking the executable

Program (How it was before I decide to go for a static dependency injection factory)

[STAThread]
static void Main() {
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    IKernel dependencies = new StandardKernel(
        new ApplicationModule(),
        new AuthenticationModule(),
        new ServiceModule(),
        new InquiriesManagementModule(),
        new DataModule());        

    ApplicationPresenter applicationPresenter = 
        dependencies.Get<ApplicationPresenter>();

    Application.Run((Form)applicationPresenter.View);
}

Program (How it is now that I use the static dependency injection factory)

[STAThread]
static void Main() {
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    DependencyInjectionFactory.Register(
        new ApplicationModule(),
        new AuthenticationModule(),
        new ServiceModule());
    ApplicationPresenter applicationPresenter = 
        DependencyInjectionFactory.Resolve<ApplicationPresenter>();

    Application.Run((Form)applicationPresenter.View);
}    

Beside, I feel like it's a dirty solution to have a static dependency injector as it can be called from anywhere as by magic...

Am I pushing it too far so that I complicate things instead of simplifying them?

回答1:

You don't need conditional or contextual bindings at all. Go for the simplest approach instead: Program the nhibernate config, session,... as if you didn't have a run-time "timing" dependency. That means: Inject the authenticated user into the NHibernate Configuration builder.

Then, you only need to ensure that the part of the object tree which relies on anything NHibernate is not instanciated before the user is authenticated. This should be easy enough.

Using any kind of bootstrapping mechanism, first create your bindings, then show the login window, do the login, upon successful login, load (show) the rest of the application.

Binding:

IBindingRoot.Bind<Configuration>().ToProvider<ConfigurationProvider>();
IBIndingRoot.Bind<ISessionFactory>()
            .ToMethod(ctx => ctx.Kernel.Get<Configuration>().BuildSessionFactory())
            .InSingletonScope();

public class ConfigurationProvider : IProvider<Configuration> 
{
    private readonly IUserService userService;

    public ConfigurationProvider(IUserService userService)
    {
        this.userService = userService;
    }

    public object Create(IContext context)
    {
        if(this.userService.AuthenticatedUser == null)
            throw new InvalidOperationException("never ever try to use NHibernate before user is authenticated! this includes injection an ISessionFactory in any class! Postpone creationg of object tree until after authentication of user. This exception means you've produced buggy code!");

        return Fluently.Configure()
            .DataBase(MsSqlConfiguration.MsSql2008)
                .ConnectionString(connectionBuilder => connectionBuilder.Is(... create the string...)
            ....
            .BuildConfiguration();
    }
}