Authorize if not in specific role attribute MVC 5 - attributes

I need authorization attribute for action which allows all but specific role.
something like
[!Authorize(Roles = "SuperUser")]
public ActionResult PaySuperUser.....
Anything built in?
Or any suggestion for custom attribute?

I think a custom attribute is a way to go.
Here is my code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
namespace YourFancyNamespace
{
public class AuthorizeExtended : AuthorizeAttribute
{
private string _notInRoles;
private List<string> _notInRolesList;
public string NotInRoles
{
get
{
return _notInRoles ?? string.Empty;
}
set
{
_notInRoles = value;
if (!string.IsNullOrWhiteSpace(_notInRoles))
{
_notInRolesList = _notInRoles
.Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries)
.Select(r => r.Trim()).ToList();
}
}
}
public override void OnAuthorization(HttpActionContext actionContext)
{
base.OnAuthorization(actionContext);
if (_notInRolesList != null && _notInRolesList.Count > 0)
{
foreach (var role in _notInRolesList)
{
if (actionContext.RequestContext.Principal.IsInRole(role))
{
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
}
}
}
}
}
And here is how you can use it:
// A AuthorizeExtended equals Authorize(with Role filter) + exclude all pesky users
[AuthorizeExtended(Roles = "User", NotInRoles="PeskyUser")]
[HttpPost]
[Route("api/Important/DoNotForgetToUpvote")]
public async Task<IHttpActionResult> DoNotForgetToUpvote()
{
return Ok("I did it!");
}
// Б AuthorizeExtended equals plain Authorize + exclude all pesky users
[AuthorizeExtended(NotInRoles="PeskyUser")]
[HttpPost]
[Route("api/Important/DoNotForgetToUpvote")]
public async Task<IHttpActionResult> DoNotForgetToUpvote()
{
return Ok("I did it!");
}
// В AuthorizeExtended equals Authorize
[AuthorizeExtended]
[HttpPost]
[Route("api/Important/DoNotForgetToUpvote")]
public async Task<IHttpActionResult> DoNotForgetToUpvote()
{
return Ok("I did it!");
}

Related

Xamarin Forms adding a placeholder to an Editor for iOS

How would you add a placeholder to an Editor in Xamarin Forms for iOS?
I tried adding through custom renderer as Control.Layer but could not find a property related to it.
Please help.
Try following code:
PCL:
using System;
using Xamarin.Forms;
namespace ABC.CustomViews
{
public class PlaceholderEditor : Editor
{
public static readonly BindableProperty PlaceholderProperty =
BindableProperty.Create<PlaceholderEditor, string>(view => view.Placeholder, String.Empty);
public PlaceholderEditor()
{
}
public string Placeholder
{
get
{
return (string)GetValue(PlaceholderProperty);
}
set
{
SetValue(PlaceholderProperty, value);
}
}
}
}
iOS (CustomeRenderer) :
using UIKit;
using ABC.CustomViews;
using ABC.iOS.CustomRenderer;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(PlaceholderEditor), typeof(PlaceholderEditorRenderer))]
namespace ABC.iOS.CustomRenderer
{
public class PlaceholderEditorRenderer : EditorRenderer
{
private string Placeholder { get; set; }
protected override void OnElementChanged(ElementChangedEventArgs<Editor> e)
{
base.OnElementChanged(e);
var element = this.Element as PlaceholderEditor;
if (Control != null && element != null)
{
Placeholder = element.Placeholder;
Control.TextColor = UIColor.LightGray;
Control.Text = Placeholder;
Control.ShouldBeginEditing += (UITextView textView) =>
{
if (textView.Text == Placeholder)
{
textView.Text = "";
textView.TextColor = UIColor.Black; // Text Color
}
return true;
};
Control.ShouldEndEditing += (UITextView textView) =>
{
if (textView.Text == "")
{
textView.Text = Placeholder;
textView.TextColor = UIColor.LightGray; // Placeholder Color
}
return true;
};
}
}
}
}
Usage :
_replyEditor = new PlaceholderEditor
{
Placeholder = "Placeholder Text"
};
below is the code for android
using System;
using MyApp;
using MyApp.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(CustomEditor), typeof(CustomEditorRenderer))]
namespace MyApp.Droid
{
public class CustomEditorRenderer : EditorRenderer
{
public CustomEditorRenderer()
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Editor> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
var element = e.NewElement as CustomEditor;
this.Control.Hint = element.Placeholder;
Control.Gravity = Android.Views.GravityFlags.Start;
Control.SetBackgroundColor(global::Android.Graphics.Color.White);
Control.SetPadding(15,15,15,15);
}
}
}
Here is a Xamarin forms version that allows a placehodler to be included in the initializer of the Editor, and also handles consistent behavior if the Text property is set in code (i.e. if Editor.Text="" it will show the placeholder in light gray.
using System;
using Xamarin.Forms;
namespace CrowdWisdom.Controls
{
public class EditorPlaceHolder : Editor
{
String placeHolderText = "";
public EditorPlaceHolder(String placeholder)
{
Text = placeholder;
TextColor = Color.LightGray;
this.Focused += EditorPlaceHolder_Focused;
this.Unfocused += EditorPlaceHolder_Unfocused;
this.placeHolderText = placeholder;
}
private void EditorPlaceHolder_Focused(object sender, FocusEventArgs e) //triggered when the user taps on the Editor to interact with it
{
if (Empty())
{
base.Text = "";
this.TextColor = Color.Black;
}
}
private void EditorPlaceHolder_Unfocused(object sender, FocusEventArgs e) //triggered when the user taps "Done" or outside of the Editor to finish the editing
{
if (Empty()) //if there is text there, leave it, if the user erased everything, put the placeholder Text back and set the TextColor to gray
{
this.Text = placeHolderText;
this.TextColor = Color.LightGray;
}
}
public String PlaceHolderText
{
get
{
return this.placeHolderText;
}
set
{
if (this.Empty())
{
this.Text = value;
this.TextColor = Color.LightGray;
}
this.placeHolderText = value;
}
}
public bool Empty()
{
return (this.Text.Equals("") || this.Text.Equals(this.placeHolderText));
}
public virtual new string Text
{
get
{
return base.Text;
}
set
{
base.Text = value;
if (Empty())
{
this.TextColor = Color.LightGray;
base.Text = this.placeHolderText;
}
else
{
this.TextColor = Color.Black;
}
}
}
}
}
Xamarin Forms adding a placeholder to an Editor for Android
using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Forms;
namespace MyCare.Renderers
{
class PlaceholderEditor : Editor
{
public static readonly BindableProperty PlaceholderProperty = BindableProperty.Create<PlaceholderEditor, string>(view => view.Placeholder, String.Empty);
public PlaceholderEditor()
{
}
public string Placeholder
{
get
{
return (string)GetValue(PlaceholderProperty);
}
set
{
SetValue(PlaceholderProperty, value);
}
}
}
}
//Renderer
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Xamarin.Forms;
using MyCare.Renderers;
using MyCare.Droid.Renderers;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(PlaceholderEditor), typeof(PlaceholderEditorRenderer))]
namespace MyCare.Droid.Renderers
{
class PlaceholderEditorRenderer : EditorRenderer
{
private string Placeholder { get; set; }
protected override void OnElementChanged(ElementChangedEventArgs<Editor> e)
{
base.OnElementChanged(e);
var element = this.Element as PlaceholderEditor;
if (Control != null && element != null)
{
Placeholder = element.Placeholder;
Control.SetTextColor(Android.Graphics.Color.Black);
Control.SetHintTextColor(Android.Graphics.Color.LightGray);
Control.Hint = element.Placeholder;
Control.SetBackgroundColor(Android.Graphics.Color.Transparent);
}
}
}
}
Inherit from Jay's solution. Here is a usage in XAML
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:cstCtrl="clr-namespace:TeamCollaXform.Views"
x:Class="TeamCollaXform.Views.MailComposePage">
...
<cstCtrl:PlaceholderEditor Grid.Row="2" x:Name="txtContent" Text="" HeightRequest="750" Placeholder="Compose..."/>

Asp.net Core code based policy needs access to Authorize attribute

In my multitenant application user permissions (read Roles if you are more comfortable with that) are set per tenant so we are adding claims to each user with value TenantName:Permission.
We are using policy based authorization with custom code using the following code
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class PermissionAuthorizeAttribute : AuthorizeAttribute
{
public Permission[] AcceptedPermissions { get; set; }
public PermissionAuthorizeAttribute()
{
}
public PermissionAuthorizeAttribute(params Permission[] acceptedPermissions)
{
AcceptedPermissions = acceptedPermissions;
Policy = "RequirePermission";
}
}
public enum Permission
{
Login = 1,
AddUser = 2,
EditOtherUser = 3,
EditBaseData = 6,
EditSettings = 7,
}
With the above code we decorate Controller actions
[PermissionAuthorize(Permission.EditSettings)]
public IActionResult Index()
In startup.cs we have
services.AddAuthorization(options =>
{
options.AddPolicy("RequirePermission", policy => policy.Requirements.Add(new PermissionRequirement()));
});
The AuthorizationHandler would in this case need access to the PermissionAuthorizeAttribute so we can check what permissions were specified on the action. At the moment we can get the attribute with the below code, but I think there must be an easier way, since there is a lot of casting an iterating there.
public class PermissionRequirement : AuthorizationHandler<PermissionRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
var filters = ((FilterContext)context.Resource).Filters;
PermissionAuthorizeAttribute permissionRequirement = null;
foreach (var filter in filters)
{
var authorizeFilter = filter as AuthorizeFilter;
if (authorizeFilter == null || authorizeFilter.AuthorizeData == null)
continue;
foreach (var item in authorizeFilter.AuthorizeData)
{
permissionRequirement = item as PermissionAuthorizeAttribute;
if (permissionRequirement != null)
break;
}
if (permissionRequirement != null)
break;
}
//TODO Check that the user has the required claims
context.Succeed(requirement);
return Task.CompletedTask;
}
}
All the examples I have found are like this, where some hardwired policy is specified in startup.cs.
services.AddAuthorization(options =>
{
options.AddPolicy("Over21",
policy => policy.Requirements.Add(new MinimumAgeRequirement(21)));
}
Here you decorate you controller or action with
[Authorize(Policy="Over21")]
public class AlcoholPurchaseRequirementsController : Controller
I think the above example would be better if you could specify the age in the controller/action, like this
[Authorize(Policy="OverAge", Age=21)]
public class AlcoholPurchaseRequirementsController : Controller
Now you need to add a different policy for every minimum age.
Any ideas on how to make this efficient?
Although i would use Resource Based Authorization as i commented, there is a way to achieve your goal:
First create a custom attribute:
public class AgeAuthorizeAttribute : Attribute
{
public int Age{ get; set; }
public AgeAuthorizeAttribute(int age)
{
Age = age;
}
}
Then write a filter provider:
public class CustomFilterProvider : IFilterProvider
{
public int Order
{
get
{
return 0;
}
}
public void OnProvidersExecuted(FilterProviderContext context)
{
}
public void OnProvidersExecuting(FilterProviderContext context)
{
var ctrl = context.ActionContext.ActionDescriptor as ControllerActionDescriptor;
var ageAttr = ctrl.MethodInfo.GetCustomAttribute<AgeAuthorizeAttribute>();
if (ageAttr == null)
{
ageAttr = ctrl.ControllerTypeInfo.GetCustomAttribute<AgeAuthorizeAttribute>();
}
if (ageAttr != null)
{
var policy = new AuthorizationPolicyBuilder()
.AddRequirements(new MinimumAgeRequirement(ageAttr.Age))
.Build();
var filter = new AuthorizeFilter(policy);
context.Results.Add(new FilterItem(new FilterDescriptor(filter, FilterScope.Action), filter));
}
}
}
Finally register filter provider:
services.TryAddEnumerable(ServiceDescriptor.Singleton<IFilterProvider, CustomFilterProvider>());
And use it
[AgeAuthorize(21)]
public IActionResult SomeAction()
... or
[AgeAuthorize(21)]
public class AlcoholPurchaseRequirementsController : Controller
ps: not tested

Custom Function to retrieve data from user table

I want a write a custom function to find a particular property from identity user model.
Say I want to find a user with a specified phone number.
How to do so..???
You need to extend the UserStore class, something like below
public interface IUserCustomStore<TUser> : IUserStore<TUser, string>, IDisposable where TUser : class, Microsoft.AspNet.Identity.IUser<string>
{
Task<TUser> FindByPhoneNumberAsync(string phoneNumber);
}
namespace AspNet.Identity.MyCustomStore
{
public class UserStore<TUser> : Microsoft.AspNet.Identity.EntityFramework.UserStore<TUser>, IUserCustomStore<TUser>
where TUser : Microsoft.AspNet.Identity.EntityFramework.IdentityUser
{
public UserStore(ApplicationDbContext context)
: base(context)
{
}
public Task<TUser> FindByPhoneNumberAsync(string phoneNumber)
{
//Your Implementation
}
}
public class UserStore<TUser> : IUserCustomStore<TUser> where TUser:IdentityUser
{
public virtual Task<TUser> FindByPhoneNumberAsync(string phoneNumber)
{
return _Users.Find(u => u.PhoneNumber == phoneNumber).FirstOrDefaultAsync();
}
}
}
Replace all occurrence of
using Microsoft.AspNet.Identity.EntityFramework
with
using AspNet.Identity.MyCustomStore
And then in the IdentityConfig.cs add a new method to ApplicationUserManager
public class ApplicationUserManager : UserManager<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store)
: base(store)
{
}
//LEAVE ALL THE METHODS AS IT IS
public virtual Task<ApplicationUser> FindByPhoneNumberUserManagerAsync(string phoneNumber)
{
IUserCustomStore<ApplicationUser> userCustomStore = this.Store as IUserCustomStore<ApplicationUser>;
if (phoneNumber == null)
{
throw new ArgumentNullException("phoneNumber");
}
return userCustomStore.FindByPhoneNumberAsync(phoneNumber);
}
}
You can now call this in the controller like
var user = await UserManager.FindByPhoneNumberUserManagerAsync(model.phoneNumber);
(assuming you have added the phoneNumber property to RegisterViewModel)
Hope this helps.

servicestack AppHostHttpListenerBase handlerpath parameter not working?

not sure if I am missing something here. I am using the AppHostHttpListenerBase in a unit test to test a service and in its constructor I pass "api" for the handlerPath parameter. I have a service registered at /hello/{Name} and am using version 3.9.17 of servicestack.
Within the Config method of my appHost class if I access
EndpointHostConfig.Instance.ServiceStackHandlerFactoryPath
it retrurns "api"
Once I am back in the unit test the same call returns null
If I try and call the service with /hello/test it works.
If I use /api/hello/test it fails
It appears that the AppHostHttpListenerBase is loosing the handlerPath ?
Does this sound like a bug or am I missing something ?
below is the code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using ServiceStack.ServiceClient.Web;
using ServiceStack.ServiceInterface;
using ServiceStack.Text;
using ServiceStack.WebHost.Endpoints;
namespace Bm.Tests
{
/// <summary>
/// Test self hosting for unit tests
/// </summary>
[TestFixture]
public class TestService
{
private TestServiceAppHost _apphost;
private const string HOST_URL = #"http://localhost:1337/";
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
_apphost = new TestServiceAppHost();
_apphost.Init();
_apphost.Start(HOST_URL);
}
[Test]
public void TestHelloServiceJson()
{
var prefix = EndpointHostConfig.Instance.ServiceStackHandlerFactoryPath;
Assert.AreEqual("api", prefix, "Should be api");
var client = new JsonServiceClient(HOST_URL);
var response = client.Send<HelloResponseTest>(new HelloTest() { Name = "Todd" });
Assert.AreEqual("Hello, Todd", response.Result);
}
[TestFixtureTearDown]
public void TestFixtureTearDown()
{
_apphost.Stop();
_apphost.Dispose();
}
}
public class HelloTest
{
public string Name { get; set; }
}
public class HelloResponseTest
{
public string Result { get; set; }
}
public class HelloServiceTest : ServiceBase<HelloTest>
{
protected override object Run(HelloTest request)
{
return new HelloResponseTest { Result = "Hello, " + request.Name };
}
}
//Define the Web Services AppHost
public class TestServiceAppHost : AppHostHttpListenerBase
{
public TestServiceAppHost() : base("testing HttpListener", "api", typeof(HelloServiceTest).Assembly) { }
public override void Configure(Funq.Container container)
{
// this works and returns api
var prefix = EndpointHostConfig.Instance.ServiceStackHandlerFactoryPath;
Routes
.Add<HelloTest>("/hello")
.Add<HelloTest>("/hello/{Name}");
}
}
}
If you want the handler root path to be /api you need to add that to the listener url, i.e:
_apphost.Start("http://localhost:1337/api/");

Configure ServiceStack Base URI

I'm creating a self-hosted REST service using service stack & AppHostHttpListenerBase. I'd like to use a base URI for my services (e.g. "api") like so:
http://myserver/api/service1/param
http://myserver/api/service2/param
How do I do this without defining "api" in each of my routes. In IIS, I can set a virtual directory to isolate the services, but how do I do this when self-hosting?
Here ya go.. (as a bonus this is how you put your service into a plugin.
using BlogEngineService;
using ServiceStack.WebHost.Endpoints;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BlogEngineWinService
{
public class AppHost : AppHostHttpListenerBase
{
public AppHost() : base("Self Host Service", typeof(AppHost).Assembly) { }
public override void Configure(Funq.Container container)
{
Plugins.Add(new BlogEngine());
}
}
}
This is how you autowire it up
The call appHost.Routes.AddFromAssembly2(typeof(HelloService).Assembly); Is what calls the extension to auto wire.
using ServiceStack.WebHost.Endpoints;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ServiceStack.ServiceInterface;
namespace BlogEngineService
{
public class BlogEngine : IPlugin, IPreInitPlugin
{
public void Register(IAppHost appHost)
{
appHost.RegisterService<HelloService>();
appHost.Routes.AddFromAssembly2(typeof(HelloService).Assembly);
}
public void Configure(IAppHost appHost)
{
}
}
}
This is how you mark the Service Class to give it a prefix.
Simply mark the class with this attribute
using ServiceStack.DataAnnotations;
using ServiceStack.ServiceHost;
using ServiceStack.ServiceInterface;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BlogEngineService
{
public class Hello
{
[PrimaryKey]
public string Bob { get; set; }
}
public class HelloResponse
{
public string Result { get; set; }
}
[PrefixedRoute("/test")]
public class HelloService : Service
{
public object Any(Hello request)
{
return new HelloResponse { Result = "Hello, " + request.Bob};
}
}
}
Create a CS file in your project for the extension..
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using ServiceStack.Common;
using ServiceStack.Common.Utils;
using ServiceStack.Common.Web;
using ServiceStack.Text;
using ServiceStack.ServiceHost;
using ServiceStack.WebHost.Endpoints;
using ServiceStack.ServiceInterface;
namespace ServiceStack.ServiceInterface
{
public static class ServiceRoutesExtensions
{
/// <summary>
/// Scans the supplied Assemblies to infer REST paths and HTTP verbs.
/// </summary>
///<param name="routes">The <see cref="IServiceRoutes"/> instance.</param>
///<param name="assembliesWithServices">
/// The assemblies with REST services.
/// </param>
/// <returns>The same <see cref="IServiceRoutes"/> instance;
/// never <see langword="null"/>.</returns>
public static IServiceRoutes AddFromAssembly2(this IServiceRoutes routes,
params Assembly[] assembliesWithServices)
{
foreach (Assembly assembly in assembliesWithServices)
{
AddNewApiRoutes(routes, assembly);
}
return routes;
}
private static void AddNewApiRoutes(IServiceRoutes routes, Assembly assembly)
{
var services = assembly.GetExportedTypes()
.Where(t => !t.IsAbstract
&& t.HasInterface(typeof(IService)));
foreach (Type service in services)
{
var allServiceActions = service.GetActions();
foreach (var requestDtoActions in allServiceActions.GroupBy(x => x.GetParameters()[0].ParameterType))
{
var requestType = requestDtoActions.Key;
var hasWildcard = requestDtoActions.Any(x => x.Name.EqualsIgnoreCase(ActionContext.AnyAction));
string allowedVerbs = null; //null == All Routes
if (!hasWildcard)
{
var allowedMethods = new List<string>();
foreach (var action in requestDtoActions)
{
allowedMethods.Add(action.Name.ToUpper());
}
if (allowedMethods.Count == 0) continue;
allowedVerbs = string.Join(" ", allowedMethods.ToArray());
}
if (service.HasAttribute<PrefixedRouteAttribute>())
{
string prefix = "";
PrefixedRouteAttribute a = (PrefixedRouteAttribute)Attribute.GetCustomAttribute(service, typeof(PrefixedRouteAttribute));
if (a.HasPrefix())
{
prefix = a.GetPrefix();
}
routes.AddRoute(requestType, allowedVerbs, prefix);
}
else
{
routes.AddRoute(requestType, allowedVerbs);
}
}
}
}
private static void AddRoute(this IServiceRoutes routes, Type requestType, string allowedVerbs, string prefix = "")
{
var newRoutes = new ServiceStack.ServiceHost.ServiceRoutes();
foreach (var strategy in EndpointHost.Config.RouteNamingConventions)
{
strategy(newRoutes, requestType, allowedVerbs);
}
foreach (var item in newRoutes.RestPaths)
{
string path = item.Path;
if (!string.IsNullOrWhiteSpace(prefix))
{
path = prefix + path;
}
routes.Add(requestType, restPath: path, verbs: allowedVerbs);
}
}
}
public class PrefixedRouteAttribute : Attribute
{
private string _prefix { get; set; }
private bool _hasPrefix { get; set; }
public PrefixedRouteAttribute(string path)
{
if (!string.IsNullOrWhiteSpace(path))
{
this._hasPrefix = true;
this._prefix = path;
//this.Path = string.Format("/{0}{1}", Prefix, Path);
}
}
public bool HasPrefix()
{
return this._hasPrefix;
}
public string GetPrefix()
{
return this._prefix;
}
}
}
ServiceStack's HttpListener hosts expects to be hosted a the root / path as the normal use-case is to have each self-hosted service available on different custom ports.
Since it doesn't currently support hosting at a /custompath, you would have to specify /api/ prefix on all your service routes.
Add an issue if you want to see support for hosting at custom paths.
There is actually an easier solution. In your web.config, update your http-handler to:
<httpHandlers>
<add path="api*" type="ServiceStack.WebHost.Endpoints.ServiceStackHttpHandlerFactory, ServiceStack" verb="*" />
</httpHandlers>
With the above, all of your service apis must be prefixed with a "/api/". If you have already used "/api/" in any of your routes, you must now remove them or have to specify it twice in your calls.
Reference:
https://github.com/ServiceStack/SocialBootstrapApi
I've found a workaround for this. I've only tested this under self hosting.
Create a 'PrefixedRouteAttribute' class that inherits from RouteAttribute
public class PrefixedRouteAttribute : RouteAttribute
{
public static string Prefix { get; set; }
public PrefixedRouteAttribute(string path) :
base(path)
{
SetPrefix();
}
public PrefixedRouteAttribute(string path, string verbs)
: base(path, verbs)
{
SetPrefix();
}
private void SetPrefix()
{
if (!string.IsNullOrWhiteSpace(Prefix))
{
this.Path = string.Format("/{0}{1}", Prefix, Path);
}
}
}
When you create your AppHost you can set your Prefix
PrefixedRouteAttribute.Prefix = "api";
Then instead of using the [Route] attribute, use the [PrefixRoute] attribute on your classes
[PrefixedRoute("/echo")]
[PrefixedRoute("/echo/{Value*}")]
public class Echo
{
[DataMember]
public string Value { get; set; }
}
This will then work for requests to
/api/echo
/api/echo/1
This could possibly be improved. I don't really like the how I need to set the Prefix via the static property but I couldn't think of a better approach under my setup. The principle of creating the overriding attribute seems sound though, and that is the important part.

Resources