I want to be able to load a user from a cloud database on each request and have that available on the request in a controller using asp.net mvc. The problem is the current framework does not support doing async operations from action filters. So OnActionExecuting, OnAuthorization methods do not allow me to do this.. for example I have the following code which DOES NOT work (so don't try it).. You get an exception : "An asynchronous module or handler completed while an asynchronous operation was still pending."
protected async override void OnAuthorization(AuthorizationContext filterContext)
{
var user = filterContext.HttpContext.User;
if (!user.Identity.IsAuthenticated)
{
HandleUnauthorizedRequest(filterContext);
return;
}
using (var session = MvcApplication.DocumentStore.OpenAsyncSession())
{
User currentUser = await session.LoadAsync<User>(user.Identity.Name);
if (currentUser == null)
{
HandleUnauthorizedRequest(filterContext);
return;
}
filterContext.HttpContext.Items["User"] = currentUser;
}
}
So is there any other way of being able to do this? I notice there is a begin execute method in the base Controller:
protected override IAsyncResult BeginExecute(RequestContext requestContext, AsyncCallback callback, object state)
{
return base.BeginExecute(requestContext, callback, state);
}
Could I do it there possibly?
The question is three months old so I guess you've managed to work around this. Anyway, I'll add my solution here, as I had to do something similar.
I used the ToAsync method from the ParallelExtensionsExtras library. This is my class:
public class AsyncControllerBase : Controller
{
protected override IAsyncResult BeginExecute(System.Web.Routing.RequestContext requestContext, AsyncCallback callback, object state)
{
return ExecuteCoreAsync(requestContext, state).ToAsync(callback, state);
}
protected override void EndExecute(IAsyncResult asyncResult)
{
IAsyncResult baseAsyncResult = ((Task<IAsyncResult>)asyncResult).Result;
base.EndExecute(baseAsyncResult);
}
protected virtual async Task<IAsyncResult> ExecuteCoreAsync(System.Web.Routing.RequestContext requestContext, object state)
{
await DoStuffHereOrInDerivedClassAsync();
var baseBeginExecuteCompletion = new TaskCompletionSource<IAsyncResult>();
AsyncCallback callback = ar =>
{
baseBeginExecuteCompletion.SetResult(ar);
};
// OnActionExecuting will be called at this point
var baseAsyncResult = base.BeginExecute(requestContext, callback, state);
await baseBeginExecuteCompletion.Task;
return baseAsyncResult;
}
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
}
}
See also this documentation from Microsoft on converting between Task and IAsyncResult.
Related
I'm trying to use the ResourceAuthorize attribute from Thinktecture.IdentityModel, but everything stops because there is no owin context.
I have a owin startup class which setups the authorization manager
[assembly: OwinStartup(typeof(My.WebApi.Startup))]
namespace My.WebApi
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
AuthConfig.Configure(app);
}
}
}
public class AuthConfig
{
public static void Configure(IAppBuilder app)
{
app.UseResourceAuthorization(new ResourceAuthorizationMiddlewareOptions
{
Manager = GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(IResourceAuthorizationManager)) as IResourceAuthorizationManager
});
}
}
and I know that it is detected and invoked. But later on, when hitting the following code from IdentityModel, I get a null pointer exception:
public static Task<bool> CheckAccessAsync(this HttpRequestMessage request, IEnumerable<Claim> actions, IEnumerable<Claim> resources)
{
var authorizationContext = new ResourceAuthorizationContext(
request.GetOwinContext().Authentication.User ?? Principal.Anonymous,
actions,
resources);
return request.CheckAccessAsync(authorizationContext);
}
I have stepped through and sees that it's caused by the GetOwinContext() returning null, since there is no MS_OwinContext or MS_OwinEnvironment property on the request.
What am I missing?
UPDATE:
I have found that i have an owin.environment property available, but it's part of the `HttpContextWrapper, not the request.
By searching around, I found some code inside of System.Web.Http.WebHost.HttpControllerHandler that looks like it should have converted the owin.environment to an MS_OwinEnvironment, but apparently, that code is never called in my case...
internal static readonly string OwinEnvironmentHttpContextKey = "owin.Environment";
internal static readonly string OwinEnvironmentKey = "MS_OwinEnvironment";
internal static HttpRequestMessage ConvertRequest(HttpContextBase httpContextBase, IHostBufferPolicySelector policySelector)
{
HttpRequestBase requestBase = httpContextBase.Request;
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethodHelper.GetHttpMethod(requestBase.HttpMethod), requestBase.Url);
bool bufferInput = policySelector == null || policySelector.UseBufferedInputStream((object) httpContextBase);
httpRequestMessage.Content = HttpControllerHandler.GetStreamContent(requestBase, bufferInput);
foreach (string str in (NameObjectCollectionBase) requestBase.Headers)
{
string[] values = requestBase.Headers.GetValues(str);
HttpControllerHandler.AddHeaderToHttpRequestMessage(httpRequestMessage, str, values);
}
HttpRequestMessageExtensions.SetHttpContext(httpRequestMessage, httpContextBase);
HttpRequestContext httpRequestContext = (HttpRequestContext) new WebHostHttpRequestContext(httpContextBase, requestBase, httpRequestMessage);
System.Net.Http.HttpRequestMessageExtensions.SetRequestContext(httpRequestMessage, httpRequestContext);
IDictionary items = httpContextBase.Items;
if (items != null && items.Contains((object) HttpControllerHandler.OwinEnvironmentHttpContextKey))
httpRequestMessage.Properties.Add(HttpControllerHandler.OwinEnvironmentKey, items[(object) HttpControllerHandler.OwinEnvironmentHttpContextKey]);
httpRequestMessage.Properties.Add(HttpPropertyKeys.RetrieveClientCertificateDelegateKey, (object) HttpControllerHandler._retrieveClientCertificate);
httpRequestMessage.Properties.Add(HttpPropertyKeys.IsLocalKey, (object) new Lazy<bool>((Func<bool>) (() => requestBase.IsLocal)));
httpRequestMessage.Properties.Add(HttpPropertyKeys.IncludeErrorDetailKey, (object) new Lazy<bool>((Func<bool>) (() => !httpContextBase.IsCustomErrorEnabled)));
return httpRequestMessage;
}
UPDATE 2:
Inside of mvc controllers, the context is available. But not in webapi controllers.
A team mate found a solution. He simply added the following line to the owin startup class:
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
Why this solves the issue is another mystery, though. But we are using wsFederation, so I guess it's needed some how. But what if we didn't use wsFed? Would we still need it to get a context? Who knows...
we use a StateServer for handling Session for the known benefits (web farm, IIS recycling).
However I am trying to figure out how to make this fault tolerant. Nothing we store in the Session is critical, it is just used for performance. So if the StateServer is not available we are happy to reload from disk.
However there appears to be no way of detecting if the StateServer is online or not, so the following code all runs fine even if the StateServer is down
try
{
//It is not NULL as it has been configured
if (HttpContext.Current.Session != null)
Session["Test"] = "value";
}
// No exception is thrown
catch (Exception)
{
throw new Exception();
}
Now it makes sense to me that no exception is thrown. The Session handling would not be very performant if it had to check the status on every write. So I am guessing what happens is that it writes all the Session vaiables when the Response is written.
There lies the problem, when it tries to write the Session it fails with a 500 error and I do not know anyway to intercept this error and handle it.
Unable to make the session state request to the session state server.
Please ensure that the ASP.NET State service is started and that the
client and server ports are the same.
What I would like to happen is that the write just fails silently (or logs an error) and clients are not impacted. As it is write now the entire site goes down due to this single point of failure.
Any ideas - am I missing something obvious?
well, it can be hard. Asp.net uses session tightly, so if session storage fails, it asp.net will also fails during initialization of session module. You can write own session state provider, that will wrap existing one, and in case of fail it will return empty session items, but it can be hard to use it, because session behavior can be unpredictable.
You can look into built in SQL session state provider, that has failover in case if your SQL server has replication.
UPDATE1
Here is example of wrapper for default session providers
public class SessionProviderWrapper : SessionStateStoreProviderBase
{
private readonly SessionStateStoreProviderBase _provider;
private static Func<SessionStateStoreProviderBase> _createProvider;
static SessionProvider()
{
_createProvider = InitializerProvider();
}
private static Func<SessionStateStoreProviderBase> InitializerProvider()
{
if (_createProvider != null)
return _createProvider;
var sessionType = "stateserver"; // you can switch to another session provider
Type type;
switch (sessionType)
{
case "inproc":
type = Type.GetType("System.Web.SessionState.InProcSessionStateStore, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
break;
case "sql":
type = Type.GetType("System.Web.SessionState.SqlSessionStateStore, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
break;
case "stateserver":
type = Type.GetType("System.Web.SessionState.OutOfProcSessionStateStore, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
break;
default:
throw new ConfigurationErrorsException("Unknow session type: " + sessionType);
}
if (type == null)
{
throw new InvalidOperationException("Failed to find session provider for " + sessionType);
}
_createProvider = GenerateConstructorCall(type);
return _createProvider;
}
private static Func<SessionStateStoreProviderBase> GenerateConstructorCall(Type type)
{
// we are searching for public constructor
var constructor = type.GetConstructors().FirstOrDefault(c => c.GetParameters().Length == 0);
if (constructor == null)
{
// otherwise for internal. SQL session provider has internal constructor, but we don't care
constructor = type.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Length == 0);
}
var node = Expression.New(constructor);
var lambda = Expression.Lambda<Func<SessionStateStoreProviderBase>>(node, null);
var func = lambda.Compile();
return func;
}
public SessionProvider()
{
var createProvider = InitializerProvider();
_provider = createProvider();
}
public override void Initialize(string name, NameValueCollection config)
{
_provider.Initialize(name, config);
}
public override string Name
{
get { return _provider.Name; }
}
public override string Description
{
get { return _provider.Description; }
}
public override void Dispose()
{
_provider.Dispose();
}
public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback)
{
return _provider.SetItemExpireCallback(expireCallback);
}
public override void InitializeRequest(HttpContext context)
{
_provider.InitializeRequest(context);
}
public override SessionStateStoreData GetItem(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId,
out SessionStateActions actions)
{
try
{
return _provider.GetItem(context, id, out locked, out lockAge, out lockId, out actions);
}
catch (Exception ex)
{
locked = false;
lockAge = TimeSpan.Zero;
lockId = null;
actions = SessionStateActions.None;
// log ex
return new SessionStateStoreData(new SessionStateItemCollection(), new HttpStaticObjectsCollection(), 10);
}
}
public override SessionStateStoreData GetItemExclusive(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId,
out SessionStateActions actions)
{
return _provider.GetItemExclusive(context, id, out locked, out lockAge, out lockId, out actions);
}
public override void ReleaseItemExclusive(HttpContext context, string id, object lockId)
{
_provider.ReleaseItemExclusive(context, id, lockId);
}
public override void SetAndReleaseItemExclusive(HttpContext context, string id, SessionStateStoreData item, object lockId, bool newItem)
{
_provider.SetAndReleaseItemExclusive(context, id, item, lockId, newItem);
}
public override void RemoveItem(HttpContext context, string id, object lockId, SessionStateStoreData item)
{
_provider.RemoveItem(context, id, lockId, item);
}
public override void ResetItemTimeout(HttpContext context, string id)
{
_provider.ResetItemTimeout(context, id);
}
public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout)
{
return _provider.CreateNewStoreData(context, timeout);
}
public override void CreateUninitializedItem(HttpContext context, string id, int timeout)
{
_provider.CreateUninitializedItem(context, id, timeout);
}
public override void EndRequest(HttpContext context)
{
_provider.EndRequest(context);
}
}
Basically you can make try\catch on each method like in GetItem method, and in case of error, you can return empty session object. If it fails in try\catch application still will be alive. But performance will be decreased as for each request it will throw a couple of exceptions on Get\Release, that will be handled in catch section. But anyway these exceptions will decrease performance a bit
I would like to accept tgolisch answer as a solution that works for me.
In Global.asax we will look for the missing StateServer error in the Application_Error event
If we find it we will use Server.ClearError() and log the error
We will also use this to log the error and possibly send out an alert
Thanks all!
I was wondering if it's possible to access the controller being executed (or about to be executed) in the SendAsync method of the DelegatingHandler? I can't seem to figure out how to get access to it, and I figure it's because it executes outside of the controller execution...
Is it possible to refer to it?
No, because message handlers operate on raw HttpRequestMessage or raw HttpResponseMessage (in case of continuations). So really, there is no concept of "current controller executing" with DelegatingHandlers since message handlers will be called before dispatching the request to the controller or (again, in the case of continuations) after the controller returns the reponse.
However, it really depends what you are trying to do.
If you want to know to which controller the request will end up getting routed, you can manually call the mechanism that would internally select the controllers.
public class MyHandler : DelegatingHandler
{
protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
var config = GlobalConfiguration.Configuration;
var controllerSelector = new DefaultHttpControllerSelector(config);
// descriptor here will contain information about the controller to which the request will be routed. If it's null (i.e. controller not found), it will throw an exception
var descriptor = controllerSelector.SelectController(request);
// continue
return base.SendAsync(request, cancellationToken);
}
}
Extending the #GalacticBoy solution, it would be better to use
public class MyHandler : DelegatingHandler
{
private static IHttpControllerSelector _controllerSelector = null;
protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
if (_controllerSelector == null)
{
var config = request.GetConfiguration();
_controllerSelector = config.Services.GetService(typeof(IHttpControllerSelector)) as IHttpControllerSelector;
}
try
{
// descriptor here will contain information about the controller to which the request will be routed. If it's null (i.e. controller not found), it will throw an exception
var descriptor = _controllerSelector.SelectController(request);
}
catch
{
// controller not found
}
// continue
return base.SendAsync(request, cancellationToken);
}
}
Depending on what your doing with the information maybe your fine with getting the information after the request is executed. For example logging the executed controller/action.
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
namespace Example
{
public class SampleHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken)
.ContinueWith(task =>
{
HttpResponseMessage response = task.Result;
string actionName = request.GetActionDescriptor().ActionName;
string controllerName = request.GetActionDescriptor().ControllerDescriptor.ControllerName;
// log action/controller or do something else
return response;
}, cancellationToken);
}
}
}
In this question & answer, I found one way to make ASP.NET MVC support asynchronous processing. However, I cannot make it work.
Basically, the idea is to create a new implementation of IRouteHandler which has only one method GetHttpHandler. The GetHttpHandler method should return an IHttpAsyncHandler implementation instead of just IHttpHandler, because IHttpAsyncHandler has Begin/EndXXXX pattern API.
public class AsyncMvcRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new AsyncMvcHandler(requestContext);
}
class AsyncMvcHandler : IHttpAsyncHandler, IRequiresSessionState
{
public AsyncMvcHandler(RequestContext context)
{
}
// IHttpHandler members
public bool IsReusable { get { return false; } }
public void ProcessRequest(HttpContext httpContext) { throw new NotImplementedException(); }
// IHttpAsyncHandler members
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
throw new NotImplementedException();
}
public void EndProcessRequest(IAsyncResult result)
{
throw new NotImplementedException();
}
}
}
Then, in the RegisterRoutes method of file Global.asax.cs, register this class AsyncMvcRouteHandler.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new Route("{controller}/{action}/{id}", new AsyncMvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
});
}
I set breakpoint at ProcessRequest, BeginProcessRequest and EndProcessRequest. Only ProcessRequest is executed. In another word, even though AsyncMvcHandler implements IHttpAsyncHandler. ASP.NET MVC doesn't know that and just handle it as an IHttpHandler implementation.
How to make ASP.NET MVC treat AsyncMvcHandler as IHttpAsyncHandler so we can have asynchronous page processing?
I had the same issue, however I found that it was because my catch all route handler:
routes.MapRoute(
"Default",
"{controller}/{action}",
new { controller = "Home", action = "Index" }
);
Was picking up the request, not the custom route I added that dealt with the async route handler. Perhaps by using the .mvc in your custom route defintion you created a distinction so that it was used rather than the synchronous catch-all.
After hours of hassle with the code, I found out the issue.
In my Visual Studio 2008, when I press Ctrl+F5, the Application Development Server is launched and IE is popped up to access "http://localhost:3573/". In this case, the sync API ProcessRequest is invoked. The stack trace is like this.
MyMvcApplication.DLL!MyMvcApplication.AsyncMvcRouteHandler.AsyncMvcHandler.ProcessRequest(System.Web.HttpContext
httpContext =
{System.Web.HttpContext}) Line 59 C#
System.Web.Mvc.dll!System.Web.Mvc.MvcHttpHandler.VerifyAndProcessRequest(System.Web.IHttpHandler
httpHandler,
System.Web.HttpContextBase
httpContext) + 0x19 bytes
System.Web.Routing.dll!System.Web.Routing.UrlRoutingHandler.ProcessRequest(System.Web.HttpContextBase
httpContext) + 0x66 bytes
System.Web.Routing.dll!System.Web.Routing.UrlRoutingHandler.ProcessRequest(System.Web.HttpContext
httpContext) + 0x28 bytes
System.Web.Routing.dll!System.Web.Routing.UrlRoutingHandler.System.Web.IHttpHandler.ProcessRequest(System.Web.HttpContext
context) + 0x8 bytes
MyMvcApplication.DLL!MyMvcApplication._Default.Page_Load(object
sender = {ASP.default_aspx},
System.EventArgs e =
{System.EventArgs}) Line 13 + 0x1a
bytes C#
However, when I change the URL in IE to be "http://localhost:3573/whatever.mvc", it hits the BeginProcessRequest. The stack trace is like this.
MyMvcApplication.DLL!MyMvcApplication.AsyncMvcRouteHandler.AsyncMvcHandler.BeginProcessRequest(System.Web.HttpContext
context = {System.Web.HttpContext},
System.AsyncCallback cb = {Method =
{Void
OnAsyncHandlerCompletion(System.IAsyncResult)}},
object extraData = null) Line 66 C#
System.Web.dll!System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
+ 0x249 bytes System.Web.dll!System.Web.HttpApplication.ExecuteStep(System.Web.HttpApplication.IExecutionStep
step =
{System.Web.HttpApplication.CallHandlerExecutionStep},
ref bool completedSynchronously =
true) + 0x9c bytes
System.Web.dll!System.Web.HttpApplication.ApplicationStepManager.ResumeSteps(System.Exception
error) + 0x133 bytes
System.Web.dll!System.Web.HttpApplication.System.Web.IHttpAsyncHandler.BeginProcessRequest(System.Web.HttpContext
context, System.AsyncCallback cb,
object extraData) + 0x7c bytes
System.Web.dll!System.Web.HttpRuntime.ProcessRequestInternal(System.Web.HttpWorkerRequest
wr =
{Microsoft.VisualStudio.WebHost.Request})
+ 0x17c bytes System.Web.dll!System.Web.HttpRuntime.ProcessRequestNoDemand(System.Web.HttpWorkerRequest
wr) + 0x63 bytes
System.Web.dll!System.Web.HttpRuntime.ProcessRequest(System.Web.HttpWorkerRequest
wr) + 0x47 bytes
WebDev.WebHost.dll!Microsoft.VisualStudio.WebHost.Request.Process()
+ 0xf1 bytes WebDev.WebHost.dll!Microsoft.VisualStudio.WebHost.Host.ProcessRequest(Microsoft.VisualStudio.WebHost.Connection
conn) + 0x4e bytes
It seems that only url with ".mvc" suffix can make asynchronous API invoked.
I have tried to do this in the past, I manage to either get the view to render and then all the async tasks would finish. Or the async tasks to finish but the view would not render.
I created a RouteCollectionExtensions based on the original MVC code. In my AsyncMvcHandler, I had an empty method (no exception) for ProcessMethod.
I am trying to wire up dependency injection with Windsor to standard asp.net web forms. I think I have achieved this using a HttpModule and a CustomAttribute (code shown below), although the solution seems a little clunky and was wondering if there is a better supported solution out of the box with Windsor?
There are several files all shown together here
// index.aspx.cs
public partial class IndexPage : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Logger.Write("page loading");
}
[Inject]
public ILogger Logger { get; set; }
}
// WindsorHttpModule.cs
public class WindsorHttpModule : IHttpModule
{
private HttpApplication _application;
private IoCProvider _iocProvider;
public void Init(HttpApplication context)
{
_application = context;
_iocProvider = context as IoCProvider;
if(_iocProvider == null)
{
throw new InvalidOperationException("Application must implement IoCProvider");
}
_application.PreRequestHandlerExecute += InitiateWindsor;
}
private void InitiateWindsor(object sender, System.EventArgs e)
{
Page currentPage = _application.Context.CurrentHandler as Page;
if(currentPage != null)
{
InjectPropertiesOn(currentPage);
currentPage.InitComplete += delegate { InjectUserControls(currentPage); };
}
}
private void InjectUserControls(Control parent)
{
if(parent.Controls != null)
{
foreach (Control control in parent.Controls)
{
if(control is UserControl)
{
InjectPropertiesOn(control);
}
InjectUserControls(control);
}
}
}
private void InjectPropertiesOn(object currentPage)
{
PropertyInfo[] properties = currentPage.GetType().GetProperties();
foreach(PropertyInfo property in properties)
{
object[] attributes = property.GetCustomAttributes(typeof (InjectAttribute), false);
if(attributes != null && attributes.Length > 0)
{
object valueToInject = _iocProvider.Container.Resolve(property.PropertyType);
property.SetValue(currentPage, valueToInject, null);
}
}
}
}
// Global.asax.cs
public class Global : System.Web.HttpApplication, IoCProvider
{
private IWindsorContainer _container;
public override void Init()
{
base.Init();
InitializeIoC();
}
private void InitializeIoC()
{
_container = new WindsorContainer();
_container.AddComponent<ILogger, Logger>();
}
public IWindsorContainer Container
{
get { return _container; }
}
}
public interface IoCProvider
{
IWindsorContainer Container { get; }
}
I think you're basically on the right track - If you have not already I would suggest taking a look at Rhino Igloo, an WebForms MVC framework, Here's a good blog post on this and the source is here - Ayende (the Author of Rhino Igloo) tackles the issue of using Windsor with webforms quite well in this project/library.
I would cache the reflection info if you're going to inject the entire nested set of controls, that could end up being a bit of a performance hog I suspect.
Last of all spring.net approaches this in a more configuration-oriented way, but it might be worth taking a look at their implementation - here's a good reference blog post on this.
Here's a modified version of the OP's code that (i) caches injected properties to avoid repeated reflection calls, (ii) releases all resolved components, (iii) encapsulates container access so as not to expose implementation.
// global.asax.cs
public class Global : HttpApplication
{
private static IWindsorContainer _container;
protected void Application_Start(object sender, EventArgs e)
{
_container = new WindsorContainer();
_container.Install(FromAssembly.This());
}
internal static object Resolve(Type type)
{
return _container.Resolve(type);
}
internal static void Release(object component)
{
_container.Release(component);
}
//...
}
// WindsorHttpModule.cs
public class WindsorHttpModule : IHttpModule
{
// cache the properties to inject for each page
private static readonly ConcurrentDictionary<Type, PropertyInfo[]> InjectedProperties = new ConcurrentDictionary<Type, PropertyInfo[]>();
private HttpApplication _context;
public void Init(HttpApplication context)
{
_context = context;
_context.PreRequestHandlerExecute += InjectProperties;
_context.EndRequest += ReleaseComponents;
}
private void InjectProperties(object sender, EventArgs e)
{
var currentPage = _context.Context.CurrentHandler as Page;
if (currentPage != null)
{
InjectProperties(currentPage);
currentPage.InitComplete += delegate { InjectUserControls(currentPage); };
}
}
private void InjectUserControls(Control parent)
{
foreach (Control control in parent.Controls)
{
if (control is UserControl)
{
InjectProperties(control);
}
InjectUserControls(control);
}
}
private void InjectProperties(Control control)
{
ResolvedComponents = new List<object>();
var pageType = control.GetType();
PropertyInfo[] properties;
if (!InjectedProperties.TryGetValue(pageType, out properties))
{
properties = control.GetType().GetProperties()
.Where(p => p.GetCustomAttributes(typeof(InjectAttribute), false).Length > 0)
.ToArray();
InjectedProperties.TryAdd(pageType, properties);
}
foreach (var property in properties)
{
var component = Global.Resolve(property.PropertyType);
property.SetValue(control, component, null);
ResolvedComponents.Add(component);
}
}
private void ReleaseComponents(object sender, EventArgs e)
{
var resolvedComponents = ResolvedComponents;
if (resolvedComponents != null)
{
foreach (var component in ResolvedComponents)
{
Global.Release(component);
}
}
}
private List<object> ResolvedComponents
{
get { return (List<object>)HttpContext.Current.Items["ResolvedComponents"]; }
set { HttpContext.Current.Items["ResolvedComponents"] = value; }
}
public void Dispose()
{ }
}
I've recently started at a company where there are a lot of legacy webform apps, so this looks to be a real interesting approach, and could offer a way forward if we wanted to add DI to existing web pages, thanks.
One point I noticed is that the Injection method uses the container.Resolve to explicitly resolve components, therefore I think we may need to do a container.Release on the components when the Page Unloads.
If we have transient components and don't do this then we may face memory leakages. Not sure how components with Per Web Request lifestyles would behave (i.e. would Windsor pick them up at the end of the web request, even though we explicitly resolved them) but here too may want to play safe.
Therefore the module may need to be extended to keep track of the components that it resolves and release them so that Windsor knows when to clean up.
One thing that was missing from the accepted answers was the fact that the http module needs to be registered in the web.config file (depending on the application) before the module will actually resolve the dependencies on the code-behind pages. What you need is :
<system.webServer>
<modules>
<add name="ClassNameForHttpModuleHere" type="NamespaceForClass"/>
</modules>
</system.webServer>
Other than that the accepted solutions worked like a charm.
Reference to the Microsoft website for adding http modules: https://msdn.microsoft.com/en-us/library/ms227673.aspx
Rather than doing it like this, you could also use a type resolver directly with something like:
ILogger Logger = ResolveType.Of<ILogger>();