Inject ISession into custom valueresolver

2019-09-18 21:23发布

问题:

I'm trying to inject an instance of ISession into a custom AutoMapper ValueResolver.

Here's the resolver

public class ContactTypeResolver 
    : ValueResolver<Common.Models.ContactType, Models.ContactType>
{
    ISession _session;

    public ContactTypeResolver(ISession session)
    {
        _session = session;
    }

    protected override Models.ContactType ResolveCore(Common.Models.ContactType source)
    {
        return _session.Load<Models.ContactType>(source.Id);
    }
}

I have a profile for setting up AutoMapper

this.CreateMap<Models.PhoneNumber, Common.Models.PhoneNumber>()
    .ReverseMap()
    .ForMember(d => d.Type, o => o.ResolveUsing<ContactTypeResolver>());

I register the resolver in a StructureMap registry like so

For<ValueResolver<Common.Models.ContactType, Models.ContactType>>()
   .Add<ContactTypeResolver>();

I am using session-per-request, and I am set the session inside of a nested container inside of StructureMapDependencyScope.cs which was created when I added StructureMap to my Web Api project. Here's the code

public void CreateNestedContainer()
{
    if (CurrentNestedContainer != null)
    {
        return;
    }
    CurrentNestedContainer = Container.GetNestedContainer();
    CurrentNestedContainer.Configure(c => c.For<ISession>().Use(ctx => ctx.GetInstance<ISessionFactory>().OpenSession()));

}

And this is how my container is set

var container = StructuremapMvc.StructureMapDependencyScope.Container;
GlobalConfiguration.Configuration.DependencyResolver = new StructureMapWebApiDependencyResolver(container);

container.Configure(x =>
{
    x.IncludeRegistry<DefaultRegistry>();
});

Mapper.Initialize(cfg =>
{
    cfg.ConstructServicesUsing(container.GetInstance);

    foreach (var profile in container.GetAllInstances<Profile>())
        cfg.AddProfile(profile);
});

I even tried using the service locator like so

this.CreateMap<Models.PhoneNumber, Common.Models.PhoneNumber>()
    .ReverseMap()
    .ForMember(d => d.Type, o => o
         .ResolveUsing<ContactTypeResolver>()
         .ConstructedBy(StructureMap.ObjectFactory.GetInstance<ContactTypeResolver>));

However, when the code runs I get a run-time exception stating

No default Instance is registered and cannot be automatically determined for type 'NHibernate.ISession'.

I also tried injecting another type registered in my container, which also did not work. What am I missing? The session instance does get injected into other objects. Also, other mappings that don't require dependency injection in the same profile work just fine.

回答1:

I believe @RadimKöhler is right.

Your parent container (where the AutoMapper types are configured) doesn't have access to the plugin types defined in the nested container. It just doesn't know about the ISession plugin type hence the exception you get.

I can think of a couple solutions here. DISCLAIMER I didn't tested them.

  1. Move the registration of the ISession type to your parent container. You should be able to scope it to HTTP requests there as well.

For example with the following registration code. I have no knowledge about ASP.NET WebAPI so there might be some differences but you get the point.

public class NHibernateRegistry : Registry
{
      public NHibernateRegistry()
      {
        For<Configuration>().Singleton().Use(c => new ConfigurationFactory().AssembleConfiguration());
        For<ISessionFactory>().Singleton().Use(c => c.GetInstance<Configuration>().BuildSessionFactory());
        For<ISession>().HybridHttpOrThreadLocalScoped().Use(c =>
        {
          var sessionFactory = c.GetInstance<ISessionFactory>();
          return sessionFactory.OpenSession();
        });
      }
    }
  1. Tell AutoMapper you want to use the nested container.


回答2:

In general, I would say, that the issue with:

No default Instance is registered ... ISession

we should solve by registering ISession inside of our container:

x.For<ISession>()
 .Use...

What would be the use? It could be our own way how to retrieve the contextual ISession. There could be code like this:

x.For<ISession>()
 .Use(() => MySessionProvider.GetCurrentSession());

This will be enough for StructureMap to properly inject this kind of instance into our Web API Service.

Behind MySessionProvider.GetCurrentSession() could be some call to static internal API which returns ISession related to HttpContext (initiated and disposed at request Start and End)

Or we can follow this:

  • NHibernate and Structure Map
  • Setting up Fluent NHibernate and StructureMap for a web application

Check this part of doc/guidance as well:

Retrieving a Service from IContext

and the subsection:

Retrieving a Service from IContext, which shows that we can do it like this

You can also retrieve other services from the IContext during object construction. Because the underlying BuildSession manages the Auto Wiring, you can generally assume that you're using the exact same object instance for a PluginType that other objects in the same object graph will receive. That's a helpful feature when you're talking about using View's within any type of desktop application or any kind of NHibernate object where the state or identity of the object requested is important.

My team uses this functionality in our NHibernate bootstrapping. We have an interface named ISessionSource that is responsible for creating the NHibernate ISession objects (it wraps a Session).

public interface ISessionSource
{
    ISession CreateSession();
}

We can't just walk up and create an ISession object directly. Instead, you have to use the ISessionSource to create an ISession for you. We still want StructureMap to inject the ISession objects into other classes, so we use the IContext.GetService<ISession>() method from within a Lambda to build ISession objects:

ForRequestedType<ISession>().TheDefault.Is.ConstructedBy(
    context => context.GetInstance<ISessionSource>().CreateSession());