Customizing Output Cache in MVC3 - c#

I have some controller actions that I want to have custom caching on. For example lets say I have a
controller action ActionResult Index(string name) {}. I want to cache on the server the HTML of this action, unless there is a "live=true" querystring parameter in the url. If that parameter is present I would like to remove that action result from the server cache and serve the response normally.
We use OutputCache(Location=OutputCacheLocation.Server) attribute to do our caching usually. Is it possible to extend this attribute somehow and make it clear the cache if the live=true parameter is present in the URL?
Are there alternative that I can use to accomplish this if I can't customize the OutputCache attribute to get the behavior I need?
UPDATE
Based on James feedback here is the code that I have:
public class LiveOutputCacheAttribute : OutputCacheAttribute
{
private const string _resetParam = "live";
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var context = filterContext.HttpContext;
AddLiveToVaryByParam();
if (context.Request[_resetParam] == "true")
{
var urlToRemove = GetUrlToRemove(filterContext);
context.Response.RemoveOutputCacheItem(urlToRemove);
return;
}
base.OnActionExecuting(filterContext);
}
private void AddLiveToVaryByParam()
{
// add live reset flag when vary by param is specified
if (VaryByParam != "*" && !VaryByParam.Contains("live"))
VaryByParam = string.Format("{0};{1}",VaryByParam, _resetParam).TrimStart(';');
}
private static string GetUrlToRemove(ActionExecutingContext filterContext)
{
var routeValues = new RouteValueDictionary(filterContext.ActionParameters);
var urlHelper = new UrlHelper(filterContext.RequestContext);
string action = filterContext.ActionDescriptor.ActionName;
string controller = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
return urlHelper.Action(action, controller, routeValues);
}
}
Here is how I use this on my action:
[LiveOutputCache(Location = OutputCacheLocation.Server, Duration = 60 * 60, VaryByParam = "name")]
public ActionResult Index(string name)
{
ViewData.Model = name + "-----" + DateTime.Now.Ticks.ToString();
return View();
}
The problem is that when I use the live=true parameter, it is still not removing the original request from the cache. Am I doing something wrong here?

You could use the VaryByParam attribute to check whether the live option is true e.g.
public class LiveOutputCacheAttribute : OutputCacheAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (VaryByParam == "true")
{
// clear cache
return;
}
base.OnActionExecuting(filterContext);
}
}
...
[LiveOutputCache(Location=OutputCacheLocation.Server, VaryByParam="live")]
public ActionResult Index(string name)
{
...
}
See How to programmatically clear outputcache for controller action method for the clearing part of it.

You can't customize OutputCacheAttribute to get the behaviour, but you could write your own CustomCacheAttribute to achive this. To do this you can get sources of OutputCacheAttribute (MVC is opensource so you can do it), copy it and rewrite function OnActionExecuting(ActionExecutingContext filterContext).

Check out my blog post on creating your own Custom Output Cache in ASP.NET MVC http://bstavroulakis.com/blog/web/custom-output-caching-in-asp-net-mvc/
I had the following expectations from the output cache which weren't met
1) Have the ability to view the Caching Object when necessary and all of its children to invalidate parts when needed.
2) Have the ability to disable caching when needed.
3) Have some logic before and after the item was cached.
4) Have some parts dynamic on the site and load only those parts, having the rest of the site static
5) Use the cache structure in other parts of the site as well.
My actions where:
To create my own CacheManager that will add/remove/find/... objects
in the cache.
public class CacheManager
{
#region ICacheManager Members
public static void Add(string key, object value, int expireSeconds)
{
if (expireSeconds == CacheManagerKey.CacheLifeSpanForever)
WebCache.Add(key, value, null, System.Web.Caching.Cache.NoAbsoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
else
WebCache.Add(key, value, null, DateTime.MaxValue, TimeSpan.FromSeconds(expireSeconds), CacheItemPriority.Normal, null);
}
public static bool Contains(string key)
{
return WebCache.Get(key) != null;
}
public static int Count()
{
return WebCache.Count;
}
public static void Insert(string key, object value)
{
WebCache.Insert(key, value);
}
public static T Get(string key)
{
return (T)WebCache.Get(key);
}
public static List GetCacheKeys()
{
List keys = new List();
foreach (DictionaryEntry entry in HttpContext.Current.Cache) keys.Add(entry.Key.ToString());
return keys;
}
public static void Remove(string key)
{
WebCache.Remove(key);
}
public static void RemoveAll()
{
List keys = GetCacheKeys();
foreach (string key in keys)
WebCache.Remove(key);
}
public object this[string key]
{
get
{
return WebCache[key];
}
set
{
WebCache[key] = value;
}
}
#endregion
public static System.Web.Caching.Cache WebCache
{
get
{
System.Web.Caching.Cache cache = null;
if (HttpContext.Current != null)
cache = HttpContext.Current.Cache;
if (cache == null)
cache = HttpRuntime.Cache;
return cache;
}
}
}
After that I created my own attribute
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
public class WebCacheAttribute : ActionFilterAttribute
{
public int Duration { get; set; }
public string CacheKey { get; set; }
public Dictionary CacheParams { get; set; }
public Type CacheReturnType { get; set; }
public string ContentType { get; set; }
public HeaderContentTypeEnum ResponseHeaderContentType{get;set;}
public string CacheObj { get; set; }
private readonly ICacheHoleFiller _cacheHoleFiller;
public WebCacheAttribute(int duration, string cacheKey, string cacheParamsStr, HeaderContentTypeEnum response = HeaderContentTypeEnum.Html, Type type = null)
{
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
}
public T GetCachedParam(Dictionary parameters, bool isAjaxRequest)
{
}
public string GetUniqueKey(bool isAjaxRequest)
{
}
public void OnException(ExceptionContext filterContext)
{
}
private HtmlTextWriter tw;
private StringWriter sw;
private StringBuilder sb;
private HttpWriter output;
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
}
}
To have some parts dynamic I used "Donut Caching"
Lastly, I created a cachehelper to call other methods in my project that will use the webcache attribute as well.
var articleStr = CacheHelper.InvokeCacheMethod(typeof(HtmlHelperExtensions), "RenderArticlesCallback", new object[] { (int)articleType });
[WebCacheAttribute(CacheManagerKey.CacheLifeSpanForever, CacheManagerKey.Page_Article_Key, "articleTypeID")]
public static string RenderArticlesCallback(int articleTypeID)
{
public static class CacheHelper
{
public delegate object SourceDataDelegate(object[] args);
public static T InvokeCacheMethod(Type type, string methodName, object[] args)
{
return (T)InvokeCacheMethod(type, methodName, null, args);
}
public static T InvokeCacheMethod(Type type, string methodName, object instance, object[] args)
{
var method = type.GetMethod(methodName);
var webCache = method.ReturnParameter.Member.GetCustomAttributes(typeof(WebCacheAttribute), true).FirstOrDefault();
Dictionary cacheParameters = FixCacheParameters(method, args);
T cachedObj;
if (Config.CacheEnabled && webCache != null)
{
cachedObj = ((WebCacheAttribute)webCache).GetCachedParam(cacheParameters, false);
if (cachedObj != null)
return cachedObj;
}
T returnObj = (T)method.Invoke(instance, args);
SaveCachedData(webCache, returnObj);
return returnObj;
}
public static void SaveCachedData(object webCache, object returnObj)
{
if (Config.CacheEnabled && webCache != null)
{
var fullParamString = ((WebCacheAttribute)webCache).GetUniqueKey(false);
CacheManager.Add(fullParamString, returnObj, ((WebCacheAttribute)webCache).Duration);
}
}
public static Dictionary FixCacheParameters(MethodInfo method, object[] args)
{
Dictionary cacheParameters = new Dictionary();
if (args != null)
{
var arguments = args.ToList();
var count = 0;
var argsCount = args.Length;
var methodParameters = method.GetParameters().ToList();
foreach (var argument in args)
{
var key = methodParameters[count].Name;
object value = null;
if (argsCount > count)
value = args[count];
if (value != null && value.GetType() == typeof(string))
value = (object)value.ToString();
if (value != null)
cacheParameters.Add(key, value);
count++;
}
}
return cacheParameters;
}
}
For more details on all of this you can visit my blog post => Custom Output Caching in ASP.NET MVC

Related

Get parameter custom mapping

I want to write many GET handlers that receive an ID for an object,
site.com/controller/Action1/1234
site.com/controller/Action2/1234
site.com/controller/Action3/1234
I would like to write the code that fetches the complex object from the DB just once:
class ComplexObject
{
public string str1 { get; set; }
public string str2 { get; set; }
}
ComplexObject GetFromId(string id)
{
ComplexObject x = Database.GetById(id);
if (x == null)
{
return Http404();
}
return x;
}
and then just use the object directly:
[Route("/[controller]/[action]/{message}")]
[HttpGet]
public string Action1(ComplexObject message)
{
return message.str1;
}
[Route("/[controller]/[action]/{message}")]
[HttpGet]
public string Action2(ComplexObject message)
{
return message.str1;
}
[Route("/[controller]/[action]/{message}")]
[HttpGet]
public string Action3(ComplexObject message)
{
return message.str1;
}
And that all of my handlers will just get the object, and won't have to check whether the ID is correct, etc.
How is that possible?
The official Microsoft Docs describe exactly how you can bind route parameters to a complex object from a database using a custom model binder.
Here's their example model binder:
public class AuthorEntityBinder : IModelBinder
{
private readonly AuthorContext _context;
public AuthorEntityBinder(AuthorContext context)
{
_context = context;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
// Try to fetch the value of the argument by name
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
var value = valueProviderResult.FirstValue;
// Check if the argument value is null or empty
if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}
if (!int.TryParse(value, out var id))
{
// Non-integer arguments result in model state errors
bindingContext.ModelState.TryAddModelError(
modelName, "Author Id must be an integer.");
return Task.CompletedTask;
}
// Model will be null if not found, including for
// out of range id values (0, -3, etc.)
var model = _context.Authors.Find(id);
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
And then there are various ways to use this new model binder. One is to add an attribute on the model itself:
[ModelBinder(BinderType = typeof(AuthorEntityBinder))]
public class Author
{
// snip
}
Another is to use an attribute on the action parameters:
[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
// snip
}
I am not sure why one would want to do what you are proposing, but it unnecessarily overcomplicates things and causes dependencies on the model binder.
Here is how I would implement this:
Have a class that manages your complex object and hide it behind an interface, the inject it into the controller:
public interface IComplexObjectManager
{
ComplexObject GetFromId(string id);
}
public class ComplexObjectManager : IComplexObjectManager
{
private readonly Database _database;
public ComplexObjectManager(Database database)
{
_database = database;
}
public ComplexObject GetFromId(string id)
{
ComplexObject x = _database.GetById(id);
return x;
}
}
[ApiController]
public class ComplexObjectController
{
public ComplexObjectController(IComplexObjectManager complexObjectManager)
{
ObjectManager = complexObjectManager;
}
public IComplexObjectManager ObjectManager { get; }
}
Then consume it in your method, changing the return type to an action result:
[Route("/[controller]/[action]/{id}")]
[HttpGet]
public IActionResult Action1(string id)
{
var obj = ObjectManager.GetFromId(id);
if(obj != null)
return Ok(obj.str1);
else
return NotFound();
}
Make sure to handle the response accordingly.
This approach decouples things (further abstraction can be added for Database), and allows for injection and unit testing.
Please check the code for consistency. I wrote this in a hurry.
I'm not doing the exactly thing that you are asking but i think it can help you. First of all, i'm using BaseController for it because you can filter your all actions before they are getting executed.
public class BaseController : Controller
{
#region /*IoC*/
public BaseViewModel baseViewModel;
public IUnitOfWork<Product> unitOfWorkProductForCart;
#endregion
#region /*ctor*/
public BaseController(IUnitOfWork<Product> unitOfWorkProductForCart)
{
this.unitOfWorkProduct = unitOfWorkProduct;
}
#endregion
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string controllerName = filterContext.ActionDescriptor.RouteValues["controller"];
string actionName = filterContext.ActionDescriptor.RouteValues["action"];
if (actionName == "ProductDetails")
{
var urlParameters = filterContext.ActionArguments;
if (urlParameters.Count != 0)
{
var isThatSlug = urlParameters.ElementAt(0).Key;
if (isThatSlug == "slug")
{
var slugCondition = urlParameters.ElementAt(0).Value;
var isThatProductExist = unitOfWorkProduct.RepositoryProduct.GetProductBySlugForChecking(slugCondition.ToString());
if (isThatProductExist.Count == 0)
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary
{
{"controller","Account"},
{"action","NotFound"}
});
}
}
}
}
}
}
in that example, i'm controlling the parameters. if it's something like i don't want, it's redirects you to the NotFound page.
i hope it can give you a idea

Application Insights - ILogger arguments rendered as name of the object in custom dimensions

Objects are rendered as strings, (name of the object), in Application Insights custom dimensions when passed as arguments to ilogger. The actual values are not shown.
Register Application Insights
services.AddApplicationInsightsTelemetry();
New log
public class HealthController : ControllerBase
{
private readonly ILogger<HealthController> _logger;
public HealthController(ILogger<HealthController> logger)
{
_logger = logger;
}
[HttpGet]
public IActionResult Get()
{
var health = new HealthViewModel()
{
ok = false
};
_logger.LogInformation("Hlep me pls {health}", health);
return Ok(health);
}
}
Result
I do not want to this this for every log:
var health = new HealthViewModel()
{
ok = false
};
_logger.LogInformation("Hlep me pls {health}", JsonConvert.SerializeObject(health));
I tried creating a middleware for application insights but the value is still the name of the object..
Why are arguments not rendered as json?
Edit
It seems like
var health = new
{
ok = false
};
_logger.LogInformation("HEJ2 {health}", health);
works but not
var health = new HealthViewModel
{
ok = false
};
_logger.LogInformation("HEJ2 {health}", health);
Not supported
Quote from https://github.com/microsoft/ApplicationInsights-dotnet/issues/1722
I think you're expecting too much of the logger. It doesn't know about JSON format, it just calls Convert.ToString on properties
Convert.ToString typically calls ToString() and the default ToString implementation for new classes is simply to return the type name
What you can do
Use ToJson() on objects logged to ILogger and create a middleware for application insights and modify the name of the log and the custom dimensions.
Middleware
public class ProcessApiTraceFilter : ITelemetryProcessor
{
private ITelemetryProcessor Next { get; set; }
private readonly IIdentity _identity;
private readonly IHostEnvironment _hostEnvironment;
public ProcessApiTraceFilter(ITelemetryProcessor next, IHostEnvironment hostEnvironment, IIdentity identity)
{
Next = next;
_identity = identity;
_hostEnvironment = hostEnvironment;
}
public void Process(ITelemetry item)
{
item.Process(_hostEnvironment, _identity);
Next.Process(item);
}
}
Implementation
public static class ApplicationInsightsExtensions
{
public static void Process(this ITelemetry item, IHostEnvironment hostEnvironment, IIdentity identity)
{
if (item is TraceTelemetry)
{
var traceTelemetry = item as TraceTelemetry;
var originalMessage = traceTelemetry.Properties.FirstOrDefault(x => x.Key == "{OriginalFormat}");
if (!string.IsNullOrEmpty(originalMessage.Key))
{
var reg = new Regex("{([A-z]*)*}", RegexOptions.Compiled);
var match = reg.Matches(originalMessage.Value);
var formattedMessage = originalMessage.Value;
foreach (Match arg in match)
{
var parameterName = arg.Value.Replace("{", "").Replace("}", "");
var parameterValue = traceTelemetry.Properties.FirstOrDefault(x => x.Key == parameterName);
formattedMessage = formattedMessage.Replace(arg.Value, "");
}
traceTelemetry.Message = formattedMessage.Trim();
}
if (identity != null)
{
var isAuthenticated = identity.IsAuthenticated();
const string customerKey = "customer";
if (isAuthenticated && !traceTelemetry.Properties.ContainsKey(customerKey))
{
var customer = identity.Customer();
if (customer != null)
{
traceTelemetry.Properties.Add(customerKey, customer.ToJson());
}
}
var request = identity.Request();
const string requestKey = "request";
if (request != null && !traceTelemetry.Properties.ContainsKey(requestKey))
{
traceTelemetry.Properties.Add(requestKey, request.ToJson());
}
}
var applicationNameKey = "applicationName";
if (hostEnvironment != null && !string.IsNullOrEmpty(hostEnvironment.ApplicationName) && !traceTelemetry.Properties.ContainsKey(applicationNameKey))
{
traceTelemetry.Properties.Add(applicationNameKey, hostEnvironment.ApplicationName);
}
}
}
}
Register application insights and middleware in startup
services.AddApplicationInsightsTelemetry();
services.AddApplicationInsightsTelemetryProcessor<ProcessApiTraceFilter>();
ToJson
public static class ObjectExtensions
{
private static readonly string Null = "null";
private static readonly string Exception = "Could not serialize object to json";
public static string ToJson(this object value, Formatting formatting = Formatting.None)
{
if (value == null) return Null;
try
{
string json = JsonConvert.SerializeObject(value, formatting);
return json;
}
catch (Exception ex)
{
return $"{Exception} - {ex?.Message}";
}
}
}
Log
//Log object? _smtpAppSettings.ToJson()
_logger.LogInformation("Email sent {to} {from} {subject}", to, _smtpAppSettings.From, subject)
Result
from your custom dimensions i can see that it`s not considering the health obj param as an extra data
_logger.LogInformation("Hlep me pls {health}", health);
trying using the jsonConverter within the string itself.
_logger.LogInformation($"Hlep me pls {JsonConvert.SerializeObject(health)}");

c# validation automation in data anontations

Summary: Im working with C# 4.5 version and more specifically in Web API.
Im trying to build an object and wrap it with attributes so when I receive a HTTP POST request, validation will be made in modelState.
a little example before code:
Lets say I have this following request object
public class PlayerRequest
{
[TeamId]
public string TeamId {set;get;}
[UserId]
public string UserId {set;get;}
}
now, I want to be able to just add an attribute to the class and it will check if class contains TeamId and UserId and if so, validate in db that in fact user has access to team.
so lets say, the declaration will be something like:
[PairsValidate]
public class TeamRequest
{
//...
}
What I aim to create is not a specific validation for TeamId and UserId but to create some sort of a pool of attribute pairs and run a simple loop to detect them and validate.
code so far:
[AttributeUsage(AttributeTargets.Class)]
public sealed class AccessValidator : ValidationAttribute
{
private readonly AttributePairValidator[] _validators =
{
UserIdTeamIdValidator.GetInstance(AccessManager.UserAccessToTeam)
};
public override bool IsValid(object value)
{
PropertyInfo[] properties = value.GetType().GetProperties();
foreach (PropertyInfo p in properties)
{
foreach (AttributePairValidator valPair in _validators)
{
valPair.Accept(/* here is the problem */ , p.GetValue as string);
}
}
}
}
public class AttributePairValidator
{
protected string fieldA;
protected string fieldB;
protected Func<string, string, Task<bool>> _validationMethod;
protected static object _lockObj = new object();
protected AttributePairValidator(Func<string, string, Task<bool>> validationMethod)
{
_validationMethod = validationMethod;
}
public bool Accept (ValidationAttribute attr, string val)
{
return true;
}
protected async Task<bool> Check()
{
if (!String.IsNullOrEmpty(fieldA) && !String.IsNullOrEmpty(fieldB))
return await _validationMethod(fieldA, fieldB);
return true;
}
}
public sealed class UserIdTeamIdValidator : AttributePairValidator
{
private static UserIdTeamIdValidator _instance = null;
private UserIdTeamIdValidator(Func<string, string, Task<bool>> validationMethod) : base (validationMethod)
{
}
public static UserIdTeamIdValidator GetInstance(Func<string, string, Task<bool>> validationMethod)
{
lock (_lockObj)
{
if (_instance == null)
_instance = new UserIdTeamIdValidator(validationMethod);
}
return _instance;
}
public async Task<bool> Accept(UserIdAttribute attr, string val)
{
fieldA = val;
return await Check();
}
public async Task<bool> Accept(TeamIdAttribute attr, string val)
{
fieldB = val;
return await Check();
}
}
other issue, if you guys already know how to solve it.
Im validating the request itself by headers and im storing some data in the actionContext's principal. In controllers i use: ActionContext.RequestContext.Principal.Identity.Name
is there any way to get this data when in validationAttribute scope?
Thanks.

Why am I receiving the exception "A public action method was not found on controller"?

I am using the following article Addition to ASP.NET MVC Localization - Using routing to support multi-culture routes.
If you look at section "Registering routes" you will see that current routes are updated (in the "RegisterRoutes" method) with the "{culture}" segment.
The difference is that I want to keep the current routes and add a duplicate for each one with a "{culture}" segment, so for a route like "foo/bar" I would get a duplicate "{culture}/foo/bar".
You can see I'm also making sure the new route comes first .
public static void MapMvcMultiCultureAttributes(this RouteCollection routes, bool inheritedRoutes = true, string defaultCulture = "en-US", string cultureCookieName = "culture")
{
routes.MapMvcAttributeRoutes(inheritedRoutes ? new InheritedRoutesProvider() : null);
var multiCultureRouteHandler = new MultiCultureMvcRouteHandler(defaultCulture, cultureCookieName);
var initialList = routes.ToList();
routes.Clear();
foreach (var routeBase in initialList)
{
var route = routeBase as Route;
if (route != null)
{
if (route.Url.StartsWith("{culture}"))
{
continue;
}
var cultureUrl = "{culture}";
if (!String.IsNullOrWhiteSpace(route.Url))
{
cultureUrl += "/" + route.Url;
}
var cultureRoute = routes.MapRoute(null, cultureUrl, null, new
{
culture = "^\\D{2,3}(-\\D{2,3})?$"
});
cultureRoute.Defaults = route.Defaults;
cultureRoute.DataTokens = route.DataTokens;
foreach (var constraint in route.Constraints)
{
cultureRoute.Constraints.Add(constraint.Key, constraint.Value);
}
cultureRoute.RouteHandler = multiCultureRouteHandler;
route.RouteHandler = multiCultureRouteHandler;
}
routes.Add(routeBase);
}
}
The "InheritedRoutesProvider" looks like this:
private class InheritedRoutesProvider : DefaultDirectRouteProvider
{
protected override IReadOnlyList<IDirectRouteFactory> GetActionRouteFactories(ActionDescriptor actionDescriptor)
{
return actionDescriptor.GetCustomAttributes(typeof(IDirectRouteFactory), true)
.Cast<IDirectRouteFactory>()
.ToArray();
}
}
My controller looks like this:
public class MyBaseController: Controller
{
[HttpGet]
[Route("bar")]
public virtual ActionResult MyAction(){
{
return Content("Hello stranger!");
}
}
[RoutePrefix("foo")]
public class MyController: MyBaseController
{
}
My "RegisterRoutes" method looks like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapMvcMultiCultureAttributes();
routes.LowercaseUrls = true;
}
Now, if I do:
/foo/bar - WORKS!
/en-US/foo/bar - HttpException A public action method 'MyAction' was not found on controller 'MyController'
I can give you an example of how I would do it. That example you are using is quite old.
Implement in your controllers (use inheritance) the BeginExecuteCore method as below:
protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
{
string cultureName = RouteData.Values["culture"] as string;
if (cultureName == null)
cultureName = Request.UserLanguages != null && Request.UserLanguages.Length > 0 ?
Request.UserLanguages[0] : // obtain it from HTTP header AcceptLanguages
null;
// Validate culture name
cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe
if (RouteData.Values["culture"] as string != cultureName)
{
// Force a valid culture in the URL
RouteData.Values["culture"] = cultureName.ToLower(); // lower case too
// Redirect user
Response.RedirectToRoute(RouteData.Values);
}
// Modify current thread's cultures
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
return base.BeginExecuteCore(callback, state);
}
Add some routes
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Custom",
url: "{controller}/{action}/{culture}",
defaults: new { culture = CultureHelper.GetDefaultCulture(), controller = "Coordinate", action = "Index" }
Implement a culture helper class
public static class CultureHelper
{
private static readonly List _cultures = new List {
"listOfClutures"
};
public static bool IsRighToLeft()
{
return System.Threading.Thread.CurrentThread.CurrentCulture.TextInfo.IsRightToLeft;
}
public static string GetImplementedCulture(string name)
{
if (string.IsNullOrEmpty(name))
return GetDefaultCulture(); // return Default culture
if (!CultureInfo.GetCultures(CultureTypes.SpecificCultures).Any(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)))
return GetDefaultCulture(); // return Default culture if it is invalid
if (_cultures.Any(c => c.Equals(name, StringComparison.InvariantCultureIgnoreCase)))
return name; // accept it
var n = GetNeutralCulture(name);
foreach (var c in _cultures)
if (c.StartsWith(n))
return c;
return GetDefaultCulture(); // return Default culture as no match found
}
public static string GetDefaultCulture()
{
return "en-GB"; // return Default culture, en-GB
}
public static string GetCurrentCulture()
{
return Thread.CurrentThread.CurrentCulture.Name;
}
public static string GetCurrentNeutralCulture()
{
return GetNeutralCulture(Thread.CurrentThread.CurrentCulture.Name);
}
public static string GetNeutralCulture(string name)
{
if (!name.Contains("-")) return name;
return name.Split('-')[0]; // Read first part only. E.g. "en", "es"
}
public static List<KeyValuePair<string, string>> GetImplementedLanguageNames()
{
List<KeyValuePair<string, string>> languageNames = new List<KeyValuePair<string, string>>();
foreach (string culture in _cultures)
{
languageNames.Add(new KeyValuePair<string, string>(culture, CultureInfo.GetCultureInfo(culture).EnglishName));
}
languageNames.Sort((firstPair, nextPair) =>
{
return firstPair.Value.CompareTo(nextPair.Value);
});
string currCulture = GetCurrentCulture();
languageNames.Remove(new KeyValuePair<string, string>(currCulture, CultureInfo.GetCultureInfo(currCulture).EnglishName));
languageNames.Insert(0, new KeyValuePair<string, string>(currCulture, CultureInfo.GetCultureInfo(currCulture).EnglishName));
return languageNames;
}
public static string GetDateTimeUsingCurrentCulture(DateTime dateToConvert)
{
CultureInfo ci = new CultureInfo(GetCurrentCulture());
return dateToConvert.ToString(ci.DateTimeFormat.ShortDatePattern + ' ' + ci.DateTimeFormat.ShortTimePattern);
}
}
I don't think you'll be able to achieve this with attribute routing because the RouteCollection passed to RegisterRoutes and its subsequent RouteData is quite different and uses internal classes that you can't get to.
I got as far as determining that RouteData expects a key called "MS_DirectRouteMatches" whose value is a collection of RouteData. Google MS_DirectRouteMatches if you want to dig further.
Ultimately, I assume this isn't intended to be an extension point.
You will get more success if you stick to conventional routing. I found your code worked pretty well in this scenario, but I expect you already knew that. Sorry I couldn't give you a better solution.
From what I can see it's not using the parent ActionResult. I'm not sure why this is happening. You could override the ActionResult in the derived class, but to me it seems that there's something amiss in the inheritance.. a fault in how you are managing your classes.
public class MyBaseController: Controller
{
[HttpGet]
[Route("bar")]
public virtual ActionResult MyAction(){
{
return Content("Hello stranger!");
}
}
So assuming this will not work:
[RoutePrefix("foo")]
public class MyController: MyBaseController
{
[HttpGet]
[Route("foo")]
public override ActionResult MyAction(){
{
return Content("Hello stranger!");
}
}
I'd suggest doing this:
[RoutePrefix("foo")]
public class MyController: MyBaseController
{
[HttpGet]
[Route("foo")]
public ActionResult MyAction(){
{
return Content("Hello stranger!");
}
}
This at least will tell you if the inheritance is flawed and you can review your code.
Another way around this would be to use the one controller with two methods for the differing formats.

Throw an custom exception and catch them with Postsharp

I have 2 attributes:
SecuredOperationAttribute
ExceptionPolicyAttribute
If user doesn't has an access to the action on controller then I throw an custom NonAuthorizedException but I can't catch it on ExceptionPolicyAttribute
My code:
[LogMethod]
[ExceptionPolicy]
public ActionResult Edit(int id)
{
// some works on here
}
[Serializable]
public class ExceptionPolicyAttribute : OnExceptionAspect
{
private ILog logger;
private string methodName;
public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
{
this.methodName = method.DeclaringType.FullName + "." + method.Name;
}
public override void OnException(MethodExecutionArgs args)
{
Guid guid = Guid.NewGuid();
var stringBuilder = new StringBuilder(1024);
// Write the exit message.
stringBuilder.Append(this.methodName);
stringBuilder.Append('(');
// Write the current instance object, unless the method
// is static.
object instance = args.Instance;
if (instance != null)
{
stringBuilder.Append("this=");
stringBuilder.Append(instance);
if (args.Arguments.Count > 0)
stringBuilder.Append("; ");
}
// Write the list of all arguments.
for (int i = 0; i < args.Arguments.Count; i++)
{
if (i > 0)
stringBuilder.Append(", ");
stringBuilder.Append(args.Arguments.GetArgument(i) ?? "null");
}
// Write the exception message.
stringBuilder.AppendFormat("): Exception ");
stringBuilder.Append(args.Exception.GetType().Name);
stringBuilder.Append(": ");
stringBuilder.Append(args.Exception.Message);
logger.Error(stringBuilder.ToString(), args.Exception);
args.FlowBehavior = FlowBehavior.Continue;
}
public override Type GetExceptionType(System.Reflection.MethodBase targetMethod)
{
return typeof(NonAuthorizedException);
}
}
And the secure attribute is:
[Serializable]
public class SecuredOperationAttribute: OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
IUserManager userManager = new UserManager();
int userId = userManager.GetUserIdFromCookie;
AdminUser adminUser = GenericSessionHelper<AdminUser>.Get(userId.ToString(), State.Session);
if(!User.CanAccess)
{
args.ReturnValue = null;
throw new NonAuthorizedException(string.Format("{0} userId li kullanıcının {1} işlemini yapmak için yetkisi yoktur",userId,args.Method.Name));
}
return;
}
}
What could be a problem? Am I using postsharp in a wrong way?
I found the solution:
I was using attributes as like :
[SecuredOperation]
[ExceptionPolicy]
public ActionResult Edit(int id)
but ExceptionPolicy couldn't catch exception. so I moved the ExceptionPolicy to top of the Controller Class:
[ExceptionPolicy]
public class UserController : BaseAuthorizedUserController
now it works.

Categories

Resources