How can I get ExtraData from OAuthWebSecurity? - oauth

(MVC 4)
AuthConfig.cs in definition;
OAuthWebSecurity.RegisterFacebookClient(
appId: "21703538509...",
appSecret: "28cbbc965e8ff6c9dc57cac9e323..."
);
ExternalLoginCallback function in, OAuthWebSecurity.VerifyAuthentication returned results are (name,link,gender). However, I need the email address. How can I get the email address from Facebook?

To get email access you need to write a custom facebook provider.
Downloaded DotNetOpenAuth.AspNet.Clients.FacebookClient and added { "scope", "email, read_stream" },
public sealed class CustomFacebookClient : OAuth2Client
{
#region Constants and Fields
/// <summary>
/// The authorization endpoint.
/// </summary>
private const string AuthorizationEndpoint = "https://www.facebook.com/dialog/oauth";
/// <summary>
/// The token endpoint.
/// </summary>
private const string TokenEndpoint = "https://graph.facebook.com/oauth/access_token";
/// <summary>
/// The _app id.
/// </summary>
private readonly string appId;
/// <summary>
/// The _app secret.
/// </summary>
private readonly string appSecret;
#endregion
#region Constructors and Destructors
/// <summary>
/// Initializes a new instance of the <see cref="FacebookClient"/> class.
/// </summary>
/// <param name="appId">
/// The app id.
/// </param>
/// <param name="appSecret">
/// The app secret.
/// </param>
public CustomFacebookClient(string appId, string appSecret)
: base("facebook")
{
Requires.NotNullOrEmpty(appId, "appId");
Requires.NotNullOrEmpty(appSecret, "appSecret");
this.appId = appId;
this.appSecret = appSecret;
}
#endregion
#region Methods
/// <summary>
/// The get service login url.
/// </summary>
/// <param name="returnUrl">
/// The return url.
/// </param>
/// <returns>An absolute URI.</returns>
protected override Uri GetServiceLoginUrl(Uri returnUrl)
{
// Note: Facebook doesn't like us to url-encode the redirect_uri value
var builder = new UriBuilder(AuthorizationEndpoint);
builder.AppendQueryArgs(
new Dictionary<string, string> {
{ "client_id", this.appId },
{ "scope", "email, read_stream" },
{ "redirect_uri", returnUrl.AbsoluteUri }
});
return builder.Uri;
}
/// <summary>
/// The get user data.
/// </summary>
/// <param name="accessToken">
/// The access token.
/// </param>
/// <returns>A dictionary of profile data.</returns>
protected override IDictionary<string, string> GetUserData(string accessToken)
{
FacebookGraphData graphData;
var request =
WebRequest.Create(
"https://graph.facebook.com/me?access_token=" + FacebookClientHelper.EscapeUriDataStringRfc3986(accessToken));
using (var response = request.GetResponse())
{
using (var responseStream = response.GetResponseStream())
{
graphData = FacebookClientHelper.Deserialize<FacebookGraphData>(responseStream);
}
}
// this dictionary must contains
var userData = new Dictionary<string, string>();
userData.AddItemIfNotEmpty("id", graphData.Id);
userData.AddItemIfNotEmpty("username", graphData.Email);
userData.AddItemIfNotEmpty("name", graphData.Name);
userData.AddItemIfNotEmpty("link", graphData.Link == null ? null : graphData.Link.AbsoluteUri);
userData.AddItemIfNotEmpty("gender", graphData.Gender);
userData.AddItemIfNotEmpty("birthday", graphData.Birthday);
userData.AddItemIfNotEmpty("email", graphData.Email);
return userData;
}
/// <summary>
/// Obtains an access token given an authorization code and callback URL.
/// </summary>
/// <param name="returnUrl">
/// The return url.
/// </param>
/// <param name="authorizationCode">
/// The authorization code.
/// </param>
/// <returns>
/// The access token.
/// </returns>
protected override string QueryAccessToken(Uri returnUrl, string authorizationCode)
{
// Note: Facebook doesn't like us to url-encode the redirect_uri value
var builder = new UriBuilder(TokenEndpoint);
builder.AppendQueryArgs(
new Dictionary<string, string> {
{ "client_id", this.appId },
{ "redirect_uri", NormalizeHexEncoding(returnUrl.AbsoluteUri) },
{ "client_secret", this.appSecret },
{ "code", authorizationCode },
});
using (WebClient client = new WebClient())
{
string data = client.DownloadString(builder.Uri);
if (string.IsNullOrEmpty(data))
{
return null;
}
var parsedQueryString = HttpUtility.ParseQueryString(data);
return parsedQueryString["access_token"];
}
}
/// <summary>
/// Converts any % encoded values in the URL to uppercase.
/// </summary>
/// <param name="url">The URL string to normalize</param>
/// <returns>The normalized url</returns>
/// <example>NormalizeHexEncoding("Login.aspx?ReturnUrl=%2fAccount%2fManage.aspx") returns "Login.aspx?ReturnUrl=%2FAccount%2FManage.aspx"</example>
/// <remarks>
/// There is an issue in Facebook whereby it will rejects the redirect_uri value if
/// the url contains lowercase % encoded values.
/// </remarks>
private static string NormalizeHexEncoding(string url)
{
var chars = url.ToCharArray();
for (int i = 0; i < chars.Length - 2; i++)
{
if (chars[i] == '%')
{
chars[i + 1] = char.ToUpperInvariant(chars[i + 1]);
chars[i + 2] = char.ToUpperInvariant(chars[i + 2]);
i += 2;
}
}
return new string(chars);
}
#endregion
}
Also, one could think this could work:
var parameters = new Dictionary<string, object>();
parameters["scope"] = "email";
OAuthWebSecurity.RegisterFacebookClient(appId: "appId", appSecret: "appSecret",
"facebook, parameters);
This doesn't work. It gives extra data for your providers, which you can use when you present your providers in views.
<img src="#provder.ExtraData["icon"]"/>

Related

ConfigurationManager RefreshInterval vs AutomaticRefreshInterval

Can someone explain to me the difference between AutomaticRefreshInterval and RefreshInterval, i'm using ConfigurationManager in my OpenIdConnectCachingSecurityTokenProvider implementing IIssuerSecurityTokenProvider, by default AutomaticRefreshInterval is set to 5 days and RefreshInterval to 30 seconds, i can't find a clear explaination between those two values, the goal is to prevent resquesting metadata frequently and request them only after 1 hours for example, is the default configuration is ok or i have to change the RefreshInterval
My OpenIdConnectCachingSecuritytokenProvider class
public class OpenIdConnectCachingSecurityTokenProvider : IIssuerSecurityTokenProvider
{
public ConfigurationManager<OpenIdConnectConfiguration> _configManager;
private string _issuer;
private IEnumerable<SecurityToken> _tokens;
private readonly string _metadataEndpoint;
private readonly ReaderWriterLockSlim _synclock = new ReaderWriterLockSlim();
public OpenIdConnectCachingSecurityTokenProvider(string metadataEndpoint)
{
_metadataEndpoint = metadataEndpoint;
_configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint);
//_configManager.RefreshInterval = new TimeSpan(0,1,0);
//_configManager.AutomaticRefreshInterval
RetrieveMetadata();
}
/// <summary>
/// Gets the issuer the credentials are for.
/// </summary>
/// <value>
/// The issuer the credentials are for.
/// </value>
public string Issuer
{
get
{
RetrieveMetadata();
_synclock.EnterReadLock();
try
{
return _issuer;
}
finally
{
_synclock.ExitReadLock();
}
}
}
/// <summary>
/// Gets all known security tokens.
/// </summary>
/// <value>
/// All known security tokens.
/// </value>
public IEnumerable<SecurityToken> SecurityTokens
{
get
{
RetrieveMetadata();
_synclock.EnterReadLock();
try
{
return _tokens;
}
finally
{
_synclock.ExitReadLock();
}
}
}
private void RetrieveMetadata()
{
_synclock.EnterWriteLock();
try
{
OpenIdConnectConfiguration config = Task.Run(_configManager.GetConfigurationAsync).Result;
_issuer = config.Issuer;
_tokens = config.SigningTokens;
}
finally
{
_synclock.ExitWriteLock();
}
}
}

How to configure Ninject in call scope binding within Azure Web Job?

I am facing a problem when try to write a web job scheduler. I am using Ninject scope binding using EF Repository pattern. But Only InSingletonScope() work as expected. How to configure it in RequestScope Or Call Scope ?
//Register Context
Kernel.Bind<MyDbContext>().ToSelf().InSingletonScope();
Kernel.Bind<IUnitOfWork<MyDbContext>>().To<UnitOfWork<MyDbContext>>().InSingletonScope();
Problem Solved From One of My Previous Post
I am posting Step By Step Solution
(1.) NinjectJobActivator
public class NinjectJobActivator : IJobActivator
{
#region Variable Declaration
private readonly IResolutionRoot _resolutionRoot;
#endregion
#region CONSTRUCTOR
/// <summary>
///
/// </summary>
/// <param name="kernel"></param>
// public NinjectJobActivator(IKernel kernel)
public NinjectJobActivator(IResolutionRoot resolutionRoot)
{
_resolutionRoot = resolutionRoot;
}
#endregion
#region CreateInstance
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T CreateInstance<T>()
{
return _resolutionRoot.Get<T>(new CallScopedParameter());
}
#endregion
}
(2) NinjectBindings
using Ninject.Extensions.Conventions;
using Ninject.Extensions.NamedScope;
public class NinjectBindings : NinjectModule
{
public override void Load()
{
//Register Context
Kernel.Bind<MyDbContext>().ToSelf()
.When(x => x.Parameters.OfType<CallScopedParameter>().Any())
.InCallScope(); // For Scheduler
Kernel.Bind<IUnitOfWork<MyDbContext>>().To<UnitOfWork<MyDbContext>>();
//Register Repository
Kernel.Bind(x => x
.FromAssemblyContaining<MyDbContext>()
.SelectAllClasses()
.InheritedFrom(typeof(IRepository<>))
.BindDefaultInterface());
}
}
(3) Program.cs
static void Main()
{
using (IKernel kernel = new StandardKernel(new NinjectBindings()))
{
var config = new JobHostConfiguration()
{
JobActivator = new NinjectJobActivator(kernel)
};
if (config.IsDevelopment)
{
config.UseDevelopmentSettings();
}
// Timer Trigger
config.UseTimers();
var host = new JobHost(config);
//// The following code ensures that the WebJob will be running continuously
host.RunAndBlock();
}
}
(4) CallScopedParameter
public sealed class CallScopedParameter : IParameter
{
/// <summary>
///
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool Equals(IParameter other)
{
if (other == null)
{
return false;
}
return other is CallScopedParameter;
}
/// <summary>
///
/// </summary>
/// <param name="context"></param>
/// <param name="target"></param>
/// <returns></returns>
public object GetValue(IContext context, ITarget target)
{
throw new NotSupportedException("this parameter does not provide a value");
}
/// <summary>
///
/// </summary>
public string Name
{
get { return typeof(CallScopedParameter).Name; }
}
/// <summary>
/// this is very important
/// </summary>
public bool ShouldInherit
{
get { return true; }
}
}
(5) Azure Web Job Function
public void DoSomething([TimerTrigger("*/30 * * * * *")] TimerInfo timer, TextWriter log)
{
try
{
var tempdetails = _sampleRepository.SearchFor(x=> DateTime.UtcNow > x.DateTo);
foreach (var detail in tempdetails)
{
if (detail.ID == 2)
{
detail.ID = 5;
}
_sampleRepository.Update(detail);
}
_unitOfWork.Commit();
}
catch (Exception ex)
{
log.WriteLine(ex.Message);
}
}

How to specify more than one IssuerSigningKey in UseJwtBearerAuthentication?

I have a REST api using OAuth bearer token authentication. Token is signed by an asymmetric key and REST api validates the token using public key. I got the code work like below. However, there is a case I need to handle when key needs to update. I am thinking to have a secondary public key passed in and let the framework validate token first using primary key and then secondary key. In this way, when I need to update key, I can easily add secondary key, swap and retire. The issue is looking at code below it only takes one signing key. Is there a way to specify multiple?
public void ConfigureAuth(IAppBuilder app)
{
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
// codes to get signningKey ignored here
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKey = new RsaSecurityKey(signingKey)
},
});
}
Thanks,
Ok, I think I figured it out. There are two ways. One simply straight forward way is to use IssuerSigningKeys property (how could I not discovery it at first place). The code looks like this:
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKeys = new List<RsaSecurityKey>
{
Utils.GetSigningKey(isPrimary: true),
Utils.GetSigningKey(isPrimary: false)
},
},
});
The second approach is to customized IOAuthBearerAuthenticationProvider. The code looks like this: First,
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AllowedAudiences = new string[] { "*" },
IssuerSecurityTokenProviders = new List<IIssuerSecurityTokenProvider>()
{
// Dummy object which won't be used anywhere. It is used to work around parameter validation error about no token provider specified.
new SymmetricKeyIssuerSecurityTokenProvider("dummy", "dummy")
},
// This is where validation work happens.
Provider = new BearerAuthenticationProvider(app)
});
Then, the BearerAuthenticationProvider class:
/// <summary>
/// Bearer authentication provider.
/// </summary>
public class BearerAuthenticationProvider : IOAuthBearerAuthenticationProvider
{
/// <summary>
/// App config.
/// </summary>
private readonly IAppBuilder appConfig;
/// <summary>
/// Handles applying the authentication challenge to the response message.
/// </summary>
public Func<OAuthChallengeContext, Task> OnApplyChallenge { get; set; }
/// <summary>
/// Handles processing OAuth bearer token.
/// </summary>
public Func<OAuthRequestTokenContext, Task> OnRequestToken { get; set; }
/// <summary>
/// Handles validating the identity produced from an OAuth bearer token.
/// </summary>
public Func<OAuthValidateIdentityContext, Task> OnValidateIdentity { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="T:Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationProvider" /> class
/// </summary>
public BearerAuthenticationProvider(IAppBuilder appConfig)
{
this.appConfig = appConfig;
this.OnRequestToken = (OAuthRequestTokenContext context) =>
{
var idContext = new OAuthValidateIdentityContext(context.OwinContext, null, null);
this.ValidateIdentity(idContext);
return Task.FromResult<int>(0);
};
this.OnValidateIdentity = (OAuthValidateIdentityContext context) => Task.FromResult<object>(null);
this.OnApplyChallenge = (OAuthChallengeContext context) => Task.FromResult<object>(null);
}
/// <summary>
/// Handles applying the authentication challenge to the response message.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public Task ApplyChallenge(OAuthChallengeContext context)
{
return this.OnApplyChallenge(context);
}
/// <summary>
/// Handles processing OAuth bearer token.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public virtual Task RequestToken(OAuthRequestTokenContext context)
{
return this.OnRequestToken(context);
}
/// <summary>
/// Handles validating the identity produced from an OAuth bearer token.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public virtual Task ValidateIdentity(OAuthValidateIdentityContext context)
{
const string AuthHeaderName = "Authorization";
if (context.Request.Headers.ContainsKey(AuthHeaderName))
{
var jwt = context.Request.Headers[AuthHeaderName].Replace("Bearer ", string.Empty);
var token = new JwtSecurityToken(jwt);
var claimIdentity = new ClaimsIdentity(token.Claims, "ExternalBearer");
var param = new TokenValidationParameters()
{
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKeys = new List<RsaSecurityKey>
{
Utils.GetSigningKey(isPrimary: true),
Utils.GetSigningKey(isPrimary: false)
},
};
SecurityToken securityToken = null;
var handler = new JwtSecurityTokenHandler();
var identity = handler.ValidateToken(token.RawData, param, out securityToken);
var claimPrincipal = new ClaimsPrincipal(claimIdentity);
context.Response.Context.Authentication.User = claimPrincipal;
context.Validated(claimIdentity);
}
else
{
throw new Exception("Invalid authorization header.");
}
return this.OnValidateIdentity(context);
}
}
First approach initializes two signing keys at app startup and only way to make change is when process restarts. the second approach retrieves keys at run time so key rollover doesn't require a service restart.
If you would like to have multiple security keys, you can use the benefit of IssuerSigningKeys property, where you can add all the keys you would like to use for authentication.

Asp.net MVC Route class that supports catch-all parameter anywhere in the URL

the more I think about it the more I believe it's possible to write a custom route that would consume these URL definitions:
{var1}/{var2}/{var3}
Const/{var1}/{var2}
Const1/{var1}/Const2/{var2}
{var1}/{var2}/Const
as well as having at most one greedy parameter on any position within any of the upper URLs like
{*var1}/{var2}/{var3}
{var1}/{*var2}/{var3}
{var1}/{var2}/{*var3}
There is one important constraint. Routes with greedy segment can't have any optional parts. All of them are mandatory.
Example
This is an exemplary request
http://localhost/Show/Topic/SubTopic/SubSubTopic/123/This-is-an-example
This is URL route definition
{action}/{*topicTree}/{id}/{title}
Algorithm
Parsing request route inside GetRouteData() should work like this:
Split request into segments:
Show
Topic
SubTopic
SubSubTopic
123
This-is-an-example
Process route URL definition starting from the left and assigning single segment values to parameters (or matching request segment values to static route constant segments).
When route segment is defined as greedy, reverse parsing and go to the last segment.
Parse route segments one by one backwards (assigning them request values) until you get to the greedy catch-all one again.
When you reach the greedy one again, join all remaining request segments (in original order) and assign them to the greedy catch-all route parameter.
Questions
As far as I can think of this, it could work. But I would like to know:
Has anyone already written this so I don't have to (because there are other aspects to parsing as well that I didn't mention (constraints, defaults etc.)
Do you see any flaws in this algorithm, because I'm going to have to write it myself if noone has done it so far.
I haven't thought about GetVirtuaPath() method at all.
Lately I'm asking questions in urgence, so I usually solve problems on my own. Sorry for that, but here's my take on the kind of route I was asking about. Anyone finds any problems with it: let me know.
Route with catch-all segment anywhere in the URL
/// <summary>
/// This route is used for cases where we want greedy route segments anywhere in the route URL definition
/// </summary>
public class GreedyRoute : Route
{
#region Properties
public new string Url { get; private set; }
private LinkedList<GreedyRouteSegment> urlSegments = new LinkedList<GreedyRouteSegment>();
private bool hasGreedySegment = false;
public int MinRequiredSegments { get; private set; }
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="VivaRoute"/> class, using the specified URL pattern and handler class.
/// </summary>
/// <param name="url">The URL pattern for the route.</param>
/// <param name="routeHandler">The object that processes requests for the route.</param>
public GreedyRoute(string url, IRouteHandler routeHandler)
: this(url, null, null, null, routeHandler)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="VivaRoute"/> class, using the specified URL pattern, handler class, and default parameter values.
/// </summary>
/// <param name="url">The URL pattern for the route.</param>
/// <param name="defaults">The values to use if the URL does not contain all the parameters.</param>
/// <param name="routeHandler">The object that processes requests for the route.</param>
public GreedyRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
: this(url, defaults, null, null, routeHandler)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="VivaRoute"/> class, using the specified URL pattern, handler class, default parameter values, and constraints.
/// </summary>
/// <param name="url">The URL pattern for the route.</param>
/// <param name="defaults">The values to use if the URL does not contain all the parameters.</param>
/// <param name="constraints">A regular expression that specifies valid values for a URL parameter.</param>
/// <param name="routeHandler">The object that processes requests for the route.</param>
public GreedyRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
: this(url, defaults, constraints, null, routeHandler)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="VivaRoute"/> class, using the specified URL pattern, handler class, default parameter values, constraints, and custom values.
/// </summary>
/// <param name="url">The URL pattern for the route.</param>
/// <param name="defaults">The values to use if the URL does not contain all the parameters.</param>
/// <param name="constraints">A regular expression that specifies valid values for a URL parameter.</param>
/// <param name="dataTokens">Custom values that are passed to the route handler, but which are not used to determine whether the route matches a specific URL pattern. The route handler might need these values to process the request.</param>
/// <param name="routeHandler">The object that processes requests for the route.</param>
public GreedyRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler)
: base(url.Replace("*", ""), defaults, constraints, dataTokens, routeHandler)
{
this.Defaults = defaults ?? new RouteValueDictionary();
this.Constraints = constraints;
this.DataTokens = dataTokens;
this.RouteHandler = routeHandler;
this.Url = url;
this.MinRequiredSegments = 0;
// URL must be defined
if (string.IsNullOrEmpty(url))
{
throw new ArgumentException("Route URL must be defined.", "url");
}
// correct URL definition can have AT MOST ONE greedy segment
if (url.Split('*').Length > 2)
{
throw new ArgumentException("Route URL can have at most one greedy segment, but not more.", "url");
}
Regex rx = new Regex(#"^(?<isToken>{)?(?(isToken)(?<isGreedy>\*?))(?<name>[a-zA-Z0-9-_]+)(?(isToken)})$", RegexOptions.Compiled | RegexOptions.Singleline);
foreach (string segment in url.Split('/'))
{
// segment must not be empty
if (string.IsNullOrEmpty(segment))
{
throw new ArgumentException("Route URL is invalid. Sequence \"//\" is not allowed.", "url");
}
if (rx.IsMatch(segment))
{
Match m = rx.Match(segment);
GreedyRouteSegment s = new GreedyRouteSegment {
IsToken = m.Groups["isToken"].Value.Length.Equals(1),
IsGreedy = m.Groups["isGreedy"].Value.Length.Equals(1),
Name = m.Groups["name"].Value
};
this.urlSegments.AddLast(s);
this.hasGreedySegment |= s.IsGreedy;
continue;
}
throw new ArgumentException("Route URL is invalid.", "url");
}
// get minimum required segments for this route
LinkedListNode<GreedyRouteSegment> seg = this.urlSegments.Last;
int sIndex = this.urlSegments.Count;
while(seg != null && this.MinRequiredSegments.Equals(0))
{
if (!seg.Value.IsToken || !this.Defaults.ContainsKey(seg.Value.Name))
{
this.MinRequiredSegments = Math.Max(this.MinRequiredSegments, sIndex);
}
sIndex--;
seg = seg.Previous;
}
// check that segments after greedy segment don't define a default
if (this.hasGreedySegment)
{
LinkedListNode<GreedyRouteSegment> s = this.urlSegments.Last;
while (s != null && !s.Value.IsGreedy)
{
if (s.Value.IsToken && this.Defaults.ContainsKey(s.Value.Name))
{
throw new ArgumentException(string.Format("Defaults for route segment \"{0}\" is not allowed, because it's specified after greedy catch-all segment.", s.Value.Name), "defaults");
}
s = s.Previous;
}
}
}
#endregion
#region GetRouteData
/// <summary>
/// Returns information about the requested route.
/// </summary>
/// <param name="httpContext">An object that encapsulates information about the HTTP request.</param>
/// <returns>
/// An object that contains the values from the route definition.
/// </returns>
public override RouteData GetRouteData(HttpContextBase httpContext)
{
string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;
RouteValueDictionary values = this.ParseRoute(virtualPath);
if (values == null)
{
return null;
}
RouteData result = new RouteData(this, this.RouteHandler);
if (!this.ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest))
{
return null;
}
// everything's fine, fill route data
foreach (KeyValuePair<string, object> value in values)
{
result.Values.Add(value.Key, value.Value);
}
if (this.DataTokens != null)
{
foreach (KeyValuePair<string, object> token in this.DataTokens)
{
result.DataTokens.Add(token.Key, token.Value);
}
}
return result;
}
#endregion
#region GetVirtualPath
/// <summary>
/// Returns information about the URL that is associated with the route.
/// </summary>
/// <param name="requestContext">An object that encapsulates information about the requested route.</param>
/// <param name="values">An object that contains the parameters for a route.</param>
/// <returns>
/// An object that contains information about the URL that is associated with the route.
/// </returns>
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
RouteUrl url = this.Bind(requestContext.RouteData.Values, values);
if (url == null)
{
return null;
}
if (!this.ProcessConstraints(requestContext.HttpContext, url.Values, RouteDirection.UrlGeneration))
{
return null;
}
VirtualPathData data = new VirtualPathData(this, url.Url);
if (this.DataTokens != null)
{
foreach (KeyValuePair<string, object> pair in this.DataTokens)
{
data.DataTokens[pair.Key] = pair.Value;
}
}
return data;
}
#endregion
#region Private methods
#region ProcessConstraints
/// <summary>
/// Processes constraints.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <param name="values">Route values.</param>
/// <param name="direction">Route direction.</param>
/// <returns><c>true</c> if constraints are satisfied; otherwise, <c>false</c>.</returns>
private bool ProcessConstraints(HttpContextBase httpContext, RouteValueDictionary values, RouteDirection direction)
{
if (this.Constraints != null)
{
foreach (KeyValuePair<string, object> constraint in this.Constraints)
{
if (!this.ProcessConstraint(httpContext, constraint.Value, constraint.Key, values, direction))
{
return false;
}
}
}
return true;
}
#endregion
#region ParseRoute
/// <summary>
/// Parses the route into segment data as defined by this route.
/// </summary>
/// <param name="virtualPath">Virtual path.</param>
/// <returns>Returns <see cref="System.Web.Routing.RouteValueDictionary"/> dictionary of route values.</returns>
private RouteValueDictionary ParseRoute(string virtualPath)
{
Stack<string> parts = new Stack<string>(virtualPath.Split(new char[] {'/'}, StringSplitOptions.RemoveEmptyEntries));
// number of request route parts must match route URL definition
if (parts.Count < this.MinRequiredSegments)
{
return null;
}
RouteValueDictionary result = new RouteValueDictionary();
// start parsing from the beginning
bool finished = false;
LinkedListNode<GreedyRouteSegment> currentSegment = this.urlSegments.First;
while (!finished && !currentSegment.Value.IsGreedy)
{
object p = parts.Pop();
if (currentSegment.Value.IsToken)
{
p = p ?? this.Defaults[currentSegment.Value.Name];
result.Add(currentSegment.Value.Name, p);
currentSegment = currentSegment.Next;
finished = currentSegment == null;
continue;
}
if (!currentSegment.Value.Equals(p))
{
return null;
}
}
// continue from the end if needed
parts = new Stack<string>(parts.Reverse());
currentSegment = this.urlSegments.Last;
while (!finished && !currentSegment.Value.IsGreedy)
{
object p = parts.Pop();
if (currentSegment.Value.IsToken)
{
p = p ?? this.Defaults[currentSegment.Value.Name];
result.Add(currentSegment.Value.Name, p);
currentSegment = currentSegment.Previous;
finished = currentSegment == null;
continue;
}
if (!currentSegment.Value.Equals(p))
{
return null;
}
}
// fill in the greedy catch-all segment
if (!finished)
{
object remaining = string.Join("/", parts.Reverse().ToArray()) ?? this.Defaults[currentSegment.Value.Name];
result.Add(currentSegment.Value.Name, remaining);
}
// add remaining default values
foreach (KeyValuePair<string, object> def in this.Defaults)
{
if (!result.ContainsKey(def.Key))
{
result.Add(def.Key, def.Value);
}
}
return result;
}
#endregion
#region Bind
/// <summary>
/// Binds the specified current values and values into a URL.
/// </summary>
/// <param name="currentValues">Current route data values.</param>
/// <param name="values">Additional route values that can be used to generate the URL.</param>
/// <returns>Returns a URL route string.</returns>
private RouteUrl Bind(RouteValueDictionary currentValues, RouteValueDictionary values)
{
currentValues = currentValues ?? new RouteValueDictionary();
values = values ?? new RouteValueDictionary();
HashSet<string> required = new HashSet<string>(this.urlSegments.Where(seg => seg.IsToken).ToList().ConvertAll(seg => seg.Name), StringComparer.OrdinalIgnoreCase);
RouteValueDictionary routeValues = new RouteValueDictionary();
object dataValue = null;
foreach (string token in new List<string>(required))
{
dataValue = values[token] ?? currentValues[token] ?? this.Defaults[token];
if (this.IsUsable(dataValue))
{
string val = dataValue as string;
if (val != null)
{
val = val.StartsWith("/") ? val.Substring(1) : val;
val = val.EndsWith("/") ? val.Substring(0, val.Length - 1) : val;
}
routeValues.Add(token, val ?? dataValue);
required.Remove(token);
}
}
// this route data is not related to this route
if (required.Count > 0)
{
return null;
}
// add all remaining values
foreach (KeyValuePair<string, object> pair1 in values)
{
if (this.IsUsable(pair1.Value) && !routeValues.ContainsKey(pair1.Key))
{
routeValues.Add(pair1.Key, pair1.Value);
}
}
// add remaining defaults
foreach (KeyValuePair<string, object> pair2 in this.Defaults)
{
if (this.IsUsable(pair2.Value) && !routeValues.ContainsKey(pair2.Key))
{
routeValues.Add(pair2.Key, pair2.Value);
}
}
// check that non-segment defaults are the same as those provided
RouteValueDictionary nonRouteDefaults = new RouteValueDictionary(this.Defaults);
foreach (GreedyRouteSegment seg in this.urlSegments.Where(ss => ss.IsToken))
{
nonRouteDefaults.Remove(seg.Name);
}
foreach (KeyValuePair<string, object> pair3 in nonRouteDefaults)
{
if (!routeValues.ContainsKey(pair3.Key) || !this.RoutePartsEqual(pair3.Value, routeValues[pair3.Key]))
{
// route data is not related to this route
return null;
}
}
StringBuilder sb = new StringBuilder();
RouteValueDictionary valuesToUse = new RouteValueDictionary(routeValues);
bool mustAdd = this.hasGreedySegment;
// build URL string
LinkedListNode<GreedyRouteSegment> s = this.urlSegments.Last;
object segmentValue = null;
while (s != null)
{
if (s.Value.IsToken)
{
segmentValue = valuesToUse[s.Value.Name];
mustAdd = mustAdd || !this.RoutePartsEqual(segmentValue, this.Defaults[s.Value.Name]);
valuesToUse.Remove(s.Value.Name);
}
else
{
segmentValue = s.Value.Name;
mustAdd = true;
}
if (mustAdd)
{
sb.Insert(0, sb.Length > 0 ? "/" : string.Empty);
sb.Insert(0, Uri.EscapeUriString(Convert.ToString(segmentValue, CultureInfo.InvariantCulture)));
}
s = s.Previous;
}
// add remaining values
if (valuesToUse.Count > 0)
{
bool first = true;
foreach (KeyValuePair<string, object> pair3 in valuesToUse)
{
// only add when different from defaults
if (!this.RoutePartsEqual(pair3.Value, this.Defaults[pair3.Key]))
{
sb.Append(first ? "?" : "&");
sb.Append(Uri.EscapeDataString(pair3.Key));
sb.Append("=");
sb.Append(Uri.EscapeDataString(Convert.ToString(pair3.Value, CultureInfo.InvariantCulture)));
first = false;
}
}
}
return new RouteUrl {
Url = sb.ToString(),
Values = routeValues
};
}
#endregion
#region IsUsable
/// <summary>
/// Determines whether an object actually is instantiated or has a value.
/// </summary>
/// <param name="value">Object value to check.</param>
/// <returns>
/// <c>true</c> if an object is instantiated or has a value; otherwise, <c>false</c>.
/// </returns>
private bool IsUsable(object value)
{
string val = value as string;
if (val != null)
{
return val.Length > 0;
}
return value != null;
}
#endregion
#region RoutePartsEqual
/// <summary>
/// Checks if two route parts are equal
/// </summary>
/// <param name="firstValue">The first value.</param>
/// <param name="secondValue">The second value.</param>
/// <returns><c>true</c> if both values are equal; otherwise, <c>false</c>.</returns>
private bool RoutePartsEqual(object firstValue, object secondValue)
{
string sFirst = firstValue as string;
string sSecond = secondValue as string;
if ((sFirst != null) && (sSecond != null))
{
return string.Equals(sFirst, sSecond, StringComparison.OrdinalIgnoreCase);
}
if ((sFirst != null) && (sSecond != null))
{
return sFirst.Equals(sSecond);
}
return (sFirst == sSecond);
}
#endregion
#endregion
}
And additional two classes that're used within upper code:
/// <summary>
/// Represents a route segment
/// </summary>
public class RouteSegment
{
/// <summary>
/// Gets or sets segment path or token name.
/// </summary>
/// <value>Route segment path or token name.</value>
public string Name { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this segment is greedy.
/// </summary>
/// <value><c>true</c> if this segment is greedy; otherwise, <c>false</c>.</value>
public bool IsGreedy { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this segment is a token.
/// </summary>
/// <value><c>true</c> if this segment is a token; otherwise, <c>false</c>.</value>
public bool IsToken { get; set; }
}
and
/// <summary>
/// Represents a generated route url with route data
/// </summary>
public class RouteUrl
{
/// <summary>
/// Gets or sets the route URL.
/// </summary>
/// <value>Route URL.</value>
public string Url { get; set; }
/// <summary>
/// Gets or sets route values.
/// </summary>
/// <value>Route values.</value>
public RouteValueDictionary Values { get; set; }
}
That's all folks. Let me know of any issues.
I've also written a blog post related to this custom route class. It explains everything into great detail.
Well. It cannot be in default hierarchy. 'cause, Routing layer splitted from actions. You cannot manipulate parameter bindings. You have to write new ActionInvoker or have to use RegEx for catching.
Global.asax:
routes.Add(new RegexRoute("Show/(?<topics>.*)/(?<id>[\\d]+)/(?<title>.*)",
new { controller = "Home", action = "Index" }));
public class RegexRoute : Route
{
private readonly Regex _regEx;
private readonly RouteValueDictionary _defaultValues;
public RegexRoute(string pattern, object defaultValues)
: this(pattern, new RouteValueDictionary(defaultValues))
{ }
public RegexRoute(string pattern, RouteValueDictionary defaultValues)
: this(pattern, defaultValues, new MvcRouteHandler())
{ }
public RegexRoute(string pattern, RouteValueDictionary defaultValues,
IRouteHandler routeHandler)
: base(null, routeHandler)
{
this._regEx = new Regex(pattern);
this._defaultValues = defaultValues;
}
private void AddDefaultValues(RouteData routeData)
{
if (this._defaultValues != null)
{
foreach (KeyValuePair<string, object> pair in this._defaultValues)
{
routeData.Values[pair.Key] = pair.Value;
}
}
}
public override RouteData GetRouteData(System.Web.HttpContextBase httpContext)
{
string requestedUrl =
httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) +
httpContext.Request.PathInfo;
Match match = _regEx.Match(requestedUrl);
if (match.Success)
{
RouteData routeData = new RouteData(this, this.RouteHandler);
AddDefaultValues(routeData);
for (int i = 0; i < match.Groups.Count; i++)
{
string key = _regEx.GroupNameFromNumber(i);
Group group = match.Groups[i];
if (!string.IsNullOrEmpty(key))
{
routeData.Values[key] = group.Value;
}
}
return routeData;
}
return null;
}
}
Controller:
public class HomeController : Controller
{
public ActionResult Index(string topics, int id, string title)
{
string[] arr = topics.Split('/')
}
}

How can I cache objects in ASP.NET MVC?

I'd like to cache objects in ASP.NET MVC. I have a BaseController that I want all Controllers to inherit from. In the BaseController there is a User property that will simply grab the User data from the database so that I can use it within the controller, or pass it to the views.
I'd like to cache this information. I'm using this information on every single page so there is no need to go to the database each page request.
I'd like something like:
if(_user is null)
GrabFromDatabase
StuffIntoCache
return CachedObject as User
How do I implement simple caching in ASP.NET MVC?
You can still use the cache (shared among all responses) and session (unique per user) for storage.
I like the following "try get from cache/create and store" pattern (c#-like pseudocode):
public static class CacheExtensions
{
public static T GetOrStore<T>(this Cache cache, string key, Func<T> generator)
{
var result = cache[key];
if(result == null)
{
result = generator();
cache[key] = result;
}
return (T)result;
}
}
you'd use this like so:
var user = HttpRuntime
.Cache
.GetOrStore<User>(
$"User{_userId}",
() => Repository.GetUser(_userId));
You can adapt this pattern to the Session, ViewState (ugh) or any other cache mechanism. You can also extend the ControllerContext.HttpContext (which I think is one of the wrappers in System.Web.Extensions), or create a new class to do it with some room for mocking the cache.
I took Will's answer and modified it to make the CacheExtensions class static and to suggest a slight alteration in order to deal with the possibility of Func<T> being null :
public static class CacheExtensions
{
private static object sync = new object();
public const int DefaultCacheExpiration = 20;
/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
/// .Cache
/// .GetOrStore<User>(
/// string.Format("User{0}", _userId),
/// () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="cache">calling object</param>
/// <param name="key">Cache key</param>
/// <param name="generator">Func that returns the object to store in cache</param>
/// <returns></returns>
/// <remarks>Uses a default cache expiration period as defined in <see cref="CacheExtensions.DefaultCacheExpiration"/></remarks>
public static T GetOrStore<T>( this Cache cache, string key, Func<T> generator ) {
return cache.GetOrStore( key, (cache[key] == null && generator != null) ? generator() : default( T ), DefaultCacheExpiration );
}
/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
/// .Cache
/// .GetOrStore<User>(
/// string.Format("User{0}", _userId),
/// () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="cache">calling object</param>
/// <param name="key">Cache key</param>
/// <param name="generator">Func that returns the object to store in cache</param>
/// <param name="expireInMinutes">Time to expire cache in minutes</param>
/// <returns></returns>
public static T GetOrStore<T>( this Cache cache, string key, Func<T> generator, double expireInMinutes ) {
return cache.GetOrStore( key, (cache[key] == null && generator != null) ? generator() : default( T ), expireInMinutes );
}
/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
/// .Cache
/// .GetOrStore<User>(
/// string.Format("User{0}", _userId),_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="cache">calling object</param>
/// <param name="key">Cache key</param>
/// <param name="obj">Object to store in cache</param>
/// <returns></returns>
/// <remarks>Uses a default cache expiration period as defined in <see cref="CacheExtensions.DefaultCacheExpiration"/></remarks>
public static T GetOrStore<T>( this Cache cache, string key, T obj ) {
return cache.GetOrStore( key, obj, DefaultCacheExpiration );
}
/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
/// .Cache
/// .GetOrStore<User>(
/// string.Format("User{0}", _userId),
/// () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="cache">calling object</param>
/// <param name="key">Cache key</param>
/// <param name="obj">Object to store in cache</param>
/// <param name="expireInMinutes">Time to expire cache in minutes</param>
/// <returns></returns>
public static T GetOrStore<T>( this Cache cache, string key, T obj, double expireInMinutes ) {
var result = cache[key];
if ( result == null ) {
lock ( sync ) {
result = cache[key];
if ( result == null ) {
result = obj != null ? obj : default( T );
cache.Insert( key, result, null, DateTime.Now.AddMinutes( expireInMinutes ), Cache.NoSlidingExpiration );
}
}
}
return (T)result;
}
}
I would also consider taking this a step further to implement a testable Session solution that extends the System.Web.HttpSessionStateBase abstract class.
public static class SessionExtension
{
/// <summary>
///
/// </summary>
/// <example><![CDATA[
/// var user = HttpContext
/// .Session
/// .GetOrStore<User>(
/// string.Format("User{0}", _userId),
/// () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="cache"></param>
/// <param name="key"></param>
/// <param name="generator"></param>
/// <returns></returns>
public static T GetOrStore<T>( this HttpSessionStateBase session, string name, Func<T> generator ) {
var result = session[name];
if ( result != null )
return (T)result;
result = generator != null ? generator() : default( T );
session.Add( name, result );
return (T)result;
}
}
If you want it cached for the length of the request, put this in your controller base class:
public User User {
get {
User _user = ControllerContext.HttpContext.Items["user"] as User;
if (_user == null) {
_user = _repository.Get<User>(id);
ControllerContext.HttpContext.Items["user"] = _user;
}
return _user;
}
}
If you want to cache for longer, use the replace the ControllerContext call with one to Cache[]. If you do choose to use the Cache object to cache longer, you'll need to use a unique cache key as it will be shared across requests/users.
A couple of the other answers here don't deal with the following:
cache stampede
double check lock
This could lead to the generator (which could take a long time) running more than once in different threads.
Here's my version that shouldn't suffer from this problem:
// using System;
// using System.Web.Caching;
// https://stackoverflow.com/a/42443437
// Usage: HttpRuntime.Cache.GetOrStore("myKey", () => GetSomethingToCache());
public static class CacheExtensions
{
private static readonly object sync = new object();
private static TimeSpan defaultExpire = TimeSpan.FromMinutes(20);
public static T GetOrStore<T>(this Cache cache, string key, Func<T> generator) =>
cache.GetOrStore(key, generator, defaultExpire);
public static T GetOrStore<T>(this Cache cache, string key, Func<T> generator, TimeSpan expire)
{
var result = cache[key];
if (result == null)
{
lock (sync)
{
result = cache[key];
if (result == null)
{
result = generator();
cache.Insert(key, result, null, DateTime.UtcNow.AddMinutes(expire.TotalMinutes), Cache.NoSlidingExpiration);
}
}
}
return (T)result;
}
}
I like to hide the fact that the data is cached in the repository. You can access the cache through the HttpContext.Current.Cache property and store the User information using "User"+id.ToString() as the key.
This means that all access to the User data from the repository will use cached data if available and requires no code changes in the model, controller, or view.
I have used this method to correct serious performance problems on a system that was querying the database for each User property and reduced page load times from minutes to single digit seconds.
#njappboy: Nice implementation. I would only defer the Generator( ) invocation until the last responsible moment. thus you can cache method invocations too.
/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
/// .Cache
/// .GetOrStore<User>(
/// string.Format("User{0}", _userId),
/// () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="Cache">calling object</param>
/// <param name="Key">Cache key</param>
/// <param name="Generator">Func that returns the object to store in cache</param>
/// <returns></returns>
/// <remarks>Uses a default cache expiration period as defined in <see cref="CacheExtensions.DefaultCacheExpiration"/></remarks>
public static T GetOrStore<T>( this Cache Cache, string Key, Func<T> Generator )
{
return Cache.GetOrStore( Key, Generator, DefaultCacheExpiration );
}
/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
/// .Cache
/// .GetOrStore<User>(
/// string.Format("User{0}", _userId),
/// () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="Cache">calling object</param>
/// <param name="Key">Cache key</param>
/// <param name="Generator">Func that returns the object to store in cache</param>
/// <param name="ExpireInMinutes">Time to expire cache in minutes</param>
/// <returns></returns>
public static T GetOrStore<T>( this Cache Cache, string Key, Func<T> Generator, double ExpireInMinutes )
{
var Result = Cache [ Key ];
if( Result == null )
{
lock( Sync )
{
if( Result == null )
{
Result = Generator( );
Cache.Insert( Key, Result, null, DateTime.Now.AddMinutes( ExpireInMinutes ), Cache.NoSlidingExpiration );
}
}
}
return ( T ) Result;
}
If you don't need specific invalidation features of ASP.NET caching, static fields are pretty good, lightweight and easy to use. However, as soon as you needed the advanced features, you can switch to ASP.NET's Cache object for storage.
The approach I use is to create a property and a private field. If the field is null, the property will fill it and return it. I also provide an InvalidateCache method that manually sets the field to null. The advantage of this approach it that the caching mechanism is encapsulated in the property and you can switch to a different approach if you want.
Implementation with a minimal cache locking. The value stored in the cache is wrapped in a container. If the value is not in the cache, then the value container is locked. The cache is locked only during the creation of the container.
public static class CacheExtensions
{
private static object sync = new object();
private class Container<T>
{
public T Value;
}
public static TValue GetOrStore<TValue>(this Cache cache, string key, Func<TValue> create, TimeSpan slidingExpiration)
{
return cache.GetOrStore(key, create, Cache.NoAbsoluteExpiration, slidingExpiration);
}
public static TValue GetOrStore<TValue>(this Cache cache, string key, Func<TValue> create, DateTime absoluteExpiration)
{
return cache.GetOrStore(key, create, absoluteExpiration, Cache.NoSlidingExpiration);
}
public static TValue GetOrStore<TValue>(this Cache cache, string key, Func<TValue> create, DateTime absoluteExpiration, TimeSpan slidingExpiration)
{
return cache.GetOrCreate(key, x => create());
}
public static TValue GetOrStore<TValue>(this Cache cache, string key, Func<string, TValue> create, DateTime absoluteExpiration, TimeSpan slidingExpiration)
{
var instance = cache.GetOrStoreContainer<TValue>(key, absoluteExpiration, slidingExpiration);
if (instance.Value == null)
lock (instance)
if (instance.Value == null)
instance.Value = create(key);
return instance.Value;
}
private static Container<TValue> GetOrStoreContainer<TValue>(this Cache cache, string key, DateTime absoluteExpiration, TimeSpan slidingExpiration)
{
var instance = cache[key];
if (instance == null)
lock (cache)
{
instance = cache[key];
if (instance == null)
{
instance = new Container<TValue>();
cache.Add(key, instance, null, absoluteExpiration, slidingExpiration, CacheItemPriority.Default, null);
}
}
return (Container<TValue>)instance;
}
}

Resources