How can I cache objects in ASP.NET MVC? - 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;
}
}

Related

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);
}
}

get current culture or browser locale on mvc 4

How do you get current culture or browser locale on MVC 4.
I find some samples which gets it from HttpContext and HttpRequest but this doesnt work on MVC 4.
How do you do it on MVC 4?
I find some samples which gets it from HttpContext and HttpRequest but this doesnt work on MVC 4.
I just love the it doesn't work problem description!!! It's like saying to a mechanic whom you don't want to pay for the job: my car doesn't work, tell me what is wrong with it so that I can fix it myself, without showing him your car of course.
Anyway, you still got the HttpRequest in your controller action. Look at the UserLanguages property:
public ActionResult SomeAction()
{
string[] userLanguages = Request.UserLanguages;
...
}
Remark: the value of this property will be null if the user agent didn't send the Accept-Languages request header. So make sure you check whether it is not null before accessing its value to avoid getting NREs.
We use the following code in NuGetGallery
/// <summary>
/// Extensions on <see cref="HttpRequest"/> and <see cref="HttpRequestBase"/>
/// </summary>
public static class HttpRequestExtensions
{
/// <summary>
/// Retrieve culture of client.
/// </summary>
/// <param name="request">Current request.</param>
/// <returns><c>null</c> if not to be determined.</returns>
public static CultureInfo DetermineClientCulture(this HttpRequest request)
{
return DetermineClientCulture(new HttpRequestWrapper(request));
}
/// <summary>
/// Retrieve culture of client.
/// </summary>
/// <param name="request">Current request.</param>
/// <returns><c>null</c> if not to be determined.</returns>
public static CultureInfo DetermineClientCulture(this HttpRequestBase request)
{
if (request == null)
{
return null;
}
string[] languages = request.UserLanguages;
if (languages == null)
{
return null;
}
//first try parse of full langcodes. Stop with first success.
foreach (string language in languages)
{
string lang = language.ToLowerInvariant().Trim();
try
{
return CultureInfo.GetCultureInfo(lang);
}
catch (CultureNotFoundException)
{
}
}
//try parse again with first 2 chars. Stop with first success.
foreach (string language in languages)
{
string lang = language.ToLowerInvariant().Trim();
if (lang.Length > 2)
{
string lang2 = lang.Substring(0, 2);
try
{
return CultureInfo.GetCultureInfo(lang2);
}
catch (CultureNotFoundException)
{
}
}
}
return null;
}
}
usage:
/// <summary>
/// Called before the action method is invoked.
/// </summary>
/// <param name="filterContext">Information about the current request and action.</param>
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!filterContext.IsChildAction)
{
//no need to do the hassle for a child action
//set the culture from the request headers
var clientCulture = Request.DetermineClientCulture();
if (clientCulture != null)
{
//and/or CurrentUICulture
Thread.CurrentThread.CurrentCulture = clientCulture;
}
}
base.OnActionExecuting(filterContext);
}

WCF services, loading data from database

I have some problem with WCF services in asp.net mvc application.
Help me please!
When I run my project some data for some entities are loading from database, but some data are not loading(Internal server error).
Code and description of exceptions:
EntityTypeController:
public HttpResponseMessage GetAll()
{
//for debug
var t = EntityTypeService.GetAll();
//some exception here!
/*
The request channel timed out while waiting for a reply after 00:00:59.9979999.
Increase the timeout value passed to the call to Request or increase the SendTimeout value on the Binding.
The time allotted to this operation may have been a portion of a longer timeout.
*/
//SendTimeout is 00:30:00 for all services.
or
/*The underlying connection was closed: A connection that was expected to be kept alive was closed by the server.*/
or
/*An error occurred while receiving the HTTP response to http://localhost:18822/Services/Department.svc.
This could be due to the service endpoint binding not using the HTTP protocol. This could also be due to an
HTTP request context being aborted by the server (possibly due to the service shutting down). See server logs for more details.*/
return CreateResponse<IEnumerable<EntityType>>(EntityTypeService.GetAll);
}
EntityTypeService:
public class EntityTypeService : BaseService<Base.Entities.EntityType>, IEntityTypeService
{
public IQueryable<Base.Entities.EntityType> GetAll()
{
var t = Repository.Get();
return t;
}
}
Repository:
public class Repository<TEntity>: IRepository<TEntity> where TEntity: BaseEntity
{
[Inject]
public IDataContext Context { get; set; }
[Inject]
public IDatabase DataSource { get; set; }
/// <summary>
/// Get entities from repository
/// </summary>
/// <param name="filter">Filter</param>
/// <param name="orderBy">Order by property</param>
/// <param name="includeProperties"></param>
/// <returns></returns>
public virtual IQueryable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
var query = Context.Set<TEntity>().AsQueryable();
// Apply filter
filter.Do((s) => {
query = query.Where(filter);
});
// Apply includes
foreach (var includeProperty in includeProperties.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
// Apply order by if need
var result = orderBy
.With(x => x(query))
.Return(x => x, query);
return result;
}
/// <summary>
/// Find entity by key
/// </summary>
/// <param name="id">Identificator</param>
/// <returns>Entity</returns>
public virtual TEntity GetById(object id)
{
//throw new NotImplementedException();
return Context.Set<TEntity>().Find(id);
}
/// <summary>
/// Add new entity to repository
/// </summary>
/// <param name="entity">New entity</param>
public virtual void Insert(TEntity entity)
{
AttachIsNotAttached(entity);
Context.Set<TEntity>().Add(entity);
Context.SaveChanges();
}
/// <summary>
/// Updated entity in repositoy
/// </summary>
/// <param name="entity">Entity</param>
public virtual void Update(TEntity entity)
{
AttachIsNotAttached(entity);
Context.Entry(entity).State = EntityState.Modified;
Context.SaveChanges();
}
/// <summary>
/// Remove entity from rpository
/// </summary>
/// <param name="entity">Entity</param>
public virtual void Delete(TEntity entity)
{
AttachIsNotAttached(entity);
Context.Set<TEntity>().Remove(entity);
Context.Entry(entity).State = EntityState.Deleted;
Context.SaveChanges();
}
/// <summary>
/// Return models error
/// </summary>
/// <returns></returns>
public IEnumerable<object> GetValidationModelErrors()
{
return Context.GetValidationErrors();
}
/// <summary>
/// Attach entity
/// </summary>
/// <param name="entity"></param>
protected void AttachIsNotAttached(TEntity entity)
{
if (Context.Entry(entity).State == EntityState.Detached)
{
var attached = Context.Set<TEntity>().Local.Where(x => x.Id == entity.Id).FirstOrDefault();
attached.Do((s) => {
Context.Entry(s).State = EntityState.Detached;
});
Context.Set<TEntity>().Attach(entity);
}
}
}
By default the wcf config has many limitations like the max size of array, and the exception doesnt exactly what happens,maybe in your case it's due to the default wcf config, I advice you to use svcTraceViewer, it will help you a lot to understand what happen exactly with an explicit messages.

How can I get ExtraData from OAuthWebSecurity?

(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"]"/>

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('/')
}
}

Resources