Disable ApiController at runtime - asp.net-web-api

I have a ASP.NET Web API (.NET 4) application which has a few controllers. We will run several instances of the Web API application on IIS with one difference. Only certain controllers will be available under certain IIS instances. What I was thinking is to disable/unload the controllers that are not applicable to an instance when the instance starts up.
Anyone got some information that could guide me in the right direction on this?

You can put your own custom IHttpControllerActivator in by decorating the DefaultHttpControllerActivator. Inside just check for a setting and only create the controller if allowed.
When you return null from the Create method the user will receive 404 Not Found message.
My example shows a value in App Settings (App.Config or Web.Config) being checked but obviously this could any other environment aware condition.
public class YourCustomControllerActivator : IHttpControllerActivator
{
private readonly IHttpControllerActivator _default = new DefaultHttpControllerActivator();
public YourCustomControllerActivator()
{
}
public IHttpController Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor,
Type controllerType)
{
if (ConfigurationManager.AppSettings["MySetting"] == "Off")
{
//Or get clever and look for attributes on the controller in controllerDescriptor.GetCustomAttributes<>();
//Or use the contoller name controllerDescriptor.ControllerName
//This example uses the type
if (controllerType == typeof (MyController) ||
controllerType == typeof (EtcController))
{
return null;
}
}
return _default.Create(request, controllerDescriptor, controllerType);
}
}
You can switch your activator in like so:
GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerActivator), new YourCustomControllerActivator());
Update
It has been a while since I looked at this question but if I was to tackle it today I would alter the approach slightly and use a custom IHttpControllerSelector. This is called before the activator and makes for a slightly more efficient place to enable and disable controllers... (although the other approach does work). You should be able to decorate or inherit from DefaultHttpControllerSelector.

Rather than unloading the controllers, I think I'd create a custom Authorize attribute that looked at the instance information in deciding to grant authorization.
You would add the following to each controller at the class level, or you could also add this to individual controller actions:
[ControllerAuthorize (AuthorizedUserSources = new[] { "IISInstance1","IISInstance2","..." })]
Here's the code for the Attribute:
public class ControllerAuthorize : AuthorizeAttribute
{
public ControllerAuthorize()
{
UnauthorizedAccessMessage = "You do not have the required access to view this content.";
}
//Property to allow array instead of single string.
private string[] _authorizedSources;
public string UnauthorizedAccessMessage { get; set; }
public string[] AuthorizedSources
{
get { return _authorizedSources ?? new string[0]; }
set { _authorizedSources = value; }
}
// return true if the IIS instance ID matches any of the AllowedSources.
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
throw new ArgumentNullException("httpContext");
//If no sources are supplied then return true, assuming none means any.
if (!AuthorizedSources.Any())
return true;
return AuthorizedSources.Any(ut => ut == httpContext.ApplicationInstance.Request.ServerVariables["INSTANCE_ID"]);
}

The IHttpControllerActivator implementation doesn't disable the routes defined using attribute routing , if you want to switch on/off a controller and have a default catch all route controller. Switching off using IHttpControllerActivator disables the controller but when the route is requested it doesn't hit the catch all route controller -it simply tries to hit the controller that was removed and returns no controller registered.

Related

TempData not kept between postback

I need some advice on how to proceed with the mvc app I'm building. On my page I type out who is logged in to the page. This I first did by creating a base class where I created a user class which contained the users name and a image representing the user. Then I passed this class on to my views. But I also need to pass other models to my views depending on what view I'm in. Sure I could build a class that contain all different models I need to use on each page but there should be a easy way to pass name and image values across the pages and be persistant? I tried TempData together with TempData.Keep() but that was not persistant. How can I pass theses values between pages?
public ActionResult Validate(AccountModels.LoginModel model)
{
if (ModelState.IsValid)
{
if (Membership.ValidateUser(model.UserName, model.Password))
{
var mu = _repo.GetUser(Membership.GetUser().ProviderUserKey.ToString());
TempData["Name"] = mu.Name;
TempData["Image"] = mu.Image;
TempData.Keep();
FormsAuthentication.RedirectFromLoginPage(model.UserName, model.RememberMe);
}
}
return View("Index");
}
As #Jyoti said, you could use of Keep() method.
To make it easy to work with TempData, I wrote these methods in my BaseController, and I use it in every controller when I need to transfer data between actions or between view and controller.
protected TReturnType GetTempDataValue<TReturnType>(PsmConstants.TempDataKey sessionName, bool peekData =false )
{
object value = peekData ? TempData.Peek(sessionName.ToString()) : TempData[sessionName.ToString()];
return (TReturnType) value;
}
protected void RemoveTempData(PsmConstants.TempDataKey sessionName)
{
if (TempData.ContainsKey(sessionName.ToString()) && TempData[sessionName.ToString()] == null) return;
TempData[sessionName.ToString()] = null;
}
protected void SetTempDataValue(PsmConstants.TempDataKey sessionName, object value)
{
if(TempData.ContainsKey(sessionName.ToString()))
TempData[sessionName.ToString()]=null;
TempData[sessionName.ToString()] = value;
}
protected void KeepTempDataValue(PsmConstants.TempDataKey sessionName)
{
if (TempData.ContainsKey(sessionName.ToString()))
TempData.Keep(sessionName.ToString());
}
And this is the Keys enumeration :
public enum TempDataKey
{
PageError = 1,
PageInfo = 2
}
And this is, the usage of these methods(Set value and Get value from TempData):
SetTempDataValue(PsmConstants.TempDataKey.PageError , 'your error message' );
var originalValues = GetTempDataValue<MyModel>(PsmConstants.TempDataKey.Info, true);
Use session instead of Temp if it is not working.but i think it should work.
TempData["Name"] = mu.Name;TempData["Image"] = mu.Image;TempData.Keep();
How you are passing this into other models,Please share the source code so that it will easy to identify.

Check if API controller method has authorize attribute

I'm trying to determine whether API method has authorize attribute and although I can easily find that using MethodInfo, I can't seem to find a way to handle check when Authorize is set on controller level.
Basically I need to know if either controller or method has Authorize attribute.
public class SecurityRequirementsOperationFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
var hasAuthorizeAttribute = context.MethodInfo
.GetCustomAttributes(true)
.OfType<AuthorizeAttribute>()
.Any();
if (!hasAuthorizeAttribute)
{
operation.Security = new List<IDictionary<string, IEnumerable<string>>>();
}
}
}
I've managed to find a way to find if method has either controller or method Authorize attribute.
var hasAuthAttribute = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
.Union(context.MethodInfo.GetCustomAttributes(true))
.OfType<AuthorizeAttribute>()
.Any();

Mvc6 versioned api actions with custom constraints

I'm making a versioned api with mvc6 and to do that I want to be able to specify for an action on which api version it should work.
My api route is: /api/{version}/... and so I want at a certain action to inspect the version route value and to see if this action is available for that version.
I want to be able to specify that as an attribute on the api action, so for example:
// This is the base api controller
[Route("api/{version:regex(^v[[0-9]].[[0-9]]$)}/[controller]")]
public abstract class ApiControllerBase { ... }
// This is an action in one of the sub classes
[HttpGet("foo")]
[ApiVersion("0.1", "0.2")] // Here! (this is params string[])
public object Foo()
{
// return
}
// This is an action in another sub class
[HttpGet("foo")]
[ApiVersion("1.0")]
public object Foo()
{
// return
}
My question is what should ApiVersion implement or extend for this to work? I don't believe action filters work as I want because I don't want to return a 404 when this doesn't match because other actions inside other controllers might be able to handle this (Later I might have HomeController with common actions and Home2Controller with extended actions that work only for 1.0).
Note that I'm not asking for an implementation of ApiVersionAttribute, I just need to know what mvc infrastructure I should hook into (action filters, route constraints, ...) that will let me create an attribute that can look into route values and say if this action is a match.
It took 4 hours analyzing the mvc6 source but it was worth it. I solved this using an action attribute implementing Microsoft.AspNet.Mvc.ActionConstraints.IActionConstraint.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class ApiVersionAttribute : Attribute, IActionConstraint
{
public ApiVersionAttribute(string version)
{
Version = version;
}
public string Version { get; }
public int Order => 0;
public bool Accept(ActionConstraintContext context)
{
var routeData = context.RouteContext.RouteData;
// return ...
}
}
And then on a certain action:
[HttpGet("foo")]
[ApiVersion("0.1")]
public object Foo()
{
// return ...
}

How to send an array via a URI using Attribute Routing in Web API?

I'm following the article on Attribute Routing in Web API 2 to try to send an array via URI:
[HttpPost("api/set/copy/{ids}")]
public HttpResponseMessage CopySet([FromUri]int[] ids)
This was working when using convention-based routing:
http://localhost:24144/api/set/copy/?ids=1&ids=2&ids=3
But with attribute routing it is no longer working - I get 404 not found.
If I try this:
http://localhost:24144/api/set/copy/1
Then it works - I get an array with one element.
How do I use attribute routing in this manner?
The behavior you are noticing is more related to Action selection & Model binding rather than Attribute Routing.
If you are expecting 'ids' to come from query string, then modify your route template like below(because the way you have defined it makes 'ids' mandatory in the uri path):
[HttpPost("api/set/copy")]
Looking at your second question, are you looking to accept a list of ids within the uri itself, like api/set/copy/[1,2,3]? if yes, I do not think web api has in-built support for this kind of model binding.
You could implement a custom parameter binding like below to achieve it though(I am guessing there are other better ways to achieve this like via modelbinders and value providers, but i am not much aware of them...so you could probably need to explore those options too):
[HttpPost("api/set/copy/{ids}")]
public HttpResponseMessage CopySet([CustomParamBinding]int[] ids)
Example:
[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
public class CustomParamBindingAttribute : ParameterBindingAttribute
{
public override HttpParameterBinding GetBinding(HttpParameterDescriptor paramDesc)
{
return new CustomParamBinding(paramDesc);
}
}
public class CustomParamBinding : HttpParameterBinding
{
public CustomParamBinding(HttpParameterDescriptor paramDesc) : base(paramDesc) { }
public override bool WillReadBody
{
get
{
return false;
}
}
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext,
CancellationToken cancellationToken)
{
//TODO: VALIDATION & ERROR CHECKS
string idsAsString = actionContext.Request.GetRouteData().Values["ids"].ToString();
idsAsString = idsAsString.Trim('[', ']');
IEnumerable<string> ids = idsAsString.Split(',');
ids = ids.Where(str => !string.IsNullOrEmpty(str));
IEnumerable<int> idList = ids.Select(strId =>
{
if (string.IsNullOrEmpty(strId)) return -1;
return Convert.ToInt32(strId);
}).ToArray();
SetValue(actionContext, idList);
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
tcs.SetResult(null);
return tcs.Task;
}
}

Silverlight localized custom validation using DataAnnotations with RIA Services

I've implemented localized validation, client-side, using the DataAnnotations attributes successfully. Now, I want to implement custom validation running server-side using the CustomValidationAttribute but my problem is that I can't find a way to get the client-side culture while executing the validation.
Here's the setup for the custom validation method:
public static ValidationResult ValidateField( string fieldValue, ValidationContext validationContext )
{
#if !SILVERLIGHT
// Get the message from the ValidationResources resx.
return new ValidationResult( ValidationResources.Message, new string[]{ "Field" } );
#else
return ValidationResult.Success;
#endif
}
This code returns the message but from the culture that the server is currently set.
I also tried to set the attribute on the property this way with same result:
[CustomValidation( typeof( CustomValidation ), "ValidateField", ErrorMessageResourceName = "Message", ErrorMessageResourceType = typeof( ValidationResources ) )]
I also tried to expose a method on my DomainService to change the Culture on the ValidationResources resx but this seems to be changing the culture not only or the current connection but for all the connections.
Since the validation is ran by Ria Services and not something I am calling directly, how can I tell the validation method to use a specific culture?
I came across this thread and I was able to fix my issue and have the culture name pass to every request made by the DomainContext (client) to the server.
First, we need to create a custom IClientMessageInspector that will be responsible to set a parameter for the CurrentUICulture for every requests.
public class AppendLanguageMessageInspector : IClientMessageInspector
{
#region IClientMessageInspector Members
public void AfterReceiveReply( ref Message reply, object correlationState )
{
// Nothing to do
}
public object BeforeSendRequest( ref Message request, IClientChannel channel )
{
var property = request.Properties[ HttpRequestMessageProperty.Name ] as HttpRequestMessageProperty;
if( property != null )
{
property.Headers[ "CultureName" ] = Thread.CurrentThread.CurrentUICulture.Name;
}
return null;
}
#endregion // IClientMessageInspector Members
}
Next, we need to create a custom WebHttpBehavior that will inject our custom IClientMessageInspector.
public class AppendLanguageHttpBehavior : WebHttpBehavior
{
public override void ApplyClientBehavior( ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime )
{
clientRuntime.MessageInspectors.Add( _inspector );
}
private readonly AppendLanguageMessageInspector _inspector = new AppendLanguageMessageInspector();
}
Finally, we extend the client DomainContext.OnCreate method to add our custom WebHttpBehavior. NOTE: The namespace of the extended DomainContext class must be the same as the generated one...
public partial class DomainService1
{
partial void OnCreated()
{
var domainClient = this.DomainClient as WebDomainClient<IDomainService1Contract>;
if( domainClient != null )
{
domainClient.ChannelFactory.Endpoint.Behaviors.Add( DomainService1.AppendLanguageHttpBehavior );
}
}
private static readonly AppendLanguageHttpBehavior AppendLanguageHttpBehavior = new AppendLanguageHttpBehavior();
}
Now, on the server-side, when we want to get the language code we can simply access it like this:
var cultureName = System.Web.HttpContext.Current.Request.Headers[ "CultureName" ];
To enjoy even more of the DataAnnotation magic, we can even change the CurrentUICulture in the Initialize of the DomainService like this:
public override void Initialize( DomainServiceContext context )
{
var cultureName = System.Web.HttpContext.Current.Request.Headers[ "UICultureName" ];
Thread.CurrentThread.CurrentUICulture = new CultureInfo( cultureName );
base.Initialize( context );
}

Resources