Localization of data annotations using IStringLocalizer - c#

I trying to implement localization in .net core 1.0 application using IStringLocalizer. I am able to do the localization for the view for which I have written something like this
private readonly IStringLocalizer<AboutController> _localizer;
public AboutController(IStringLocalizer<AboutController> localizer)
{
_localizer = localizer;
}
public IActionResult About()
{
ViewBag.Name = _localizer["Name"];
Return View();
}
So this is working fine, however I am curious how can I use IStringLocalizer in CustomAttribute from where I will be getting localized validation message.
Model
public partial class LMS_User
{
[RequiredFieldValidator("FirstNameRequired")]
public string FirstName { get; set; }
[RequiredFieldValidator("LastNameRequired")]
public string LastName { get; set; }
}
from model I have passed the resource key to custom attribute where I will be retrieving the localized message.
Custom Attribute
public class RequiredFieldValidator: ValidationAttribute , IClientModelValidator
{
private readonly string resourcekey = string.Empty;
public RequiredFieldValidator(string resourceID)
{
resourcekey = resourceID;
}
}
public void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Here I want to get localized message using SQL.
var errorMessage = "This field is required field.";
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data- val-Required",errorMessage);
}
private static bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
{
if (attributes.ContainsKey(key))
{
return false;
}
attributes.Add(key, value);
return true;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
return ValidationResult.Success;
}
So, how can I use IStringLocalizer in custom attribute ? I want to do this using SQL.
Any help on this appreaciated !

I like to implement localization as a service.
public RequiredFieldValidator(IStringLocalizer localizationService, string resourceID)
{
resourcekey = resourceID;
localization = localizationService;
}
public void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Here I want to get localized message using SQL.
var errorMessage = lozalization["requiredFieldMessage"];
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data- val-Required",errorMessage);
}
you can choose implement the interface using resource strings, accessing to database to get the translations,... Here i'm implementing one method accessing to resource strings, assuming that the resources are in the same project.
public class LocalizationService : IStringLocalizer {
public LocalizedString this[string name] {
return new LocalizedString(name, Properties.Resources.GetString(name));
}
//implement the rest of methods of IStringLocalizer
}

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

Dependency Injection for custom validation attributes

I created a custom validation attribute that I want to use for my API controller DTOs. This attribute needs values from the configured options, that's why I'm injecting them in the constructor, so that I can use the options service later on in the IsValid and FormatErrorMessage method.
internal class MyValidationAttribute : ValidationAttribute
{
private readonly IOptionsMonitor<MyOptions> myOptionsMonitor;
public MyValidationAttribute(IOptionsMonitor<MyOptions> myOptionsMonitor)
{
this.myOptionsMonitor = myOptionsMonitor;
}
public override bool IsValid(object value)
{
// ... use myOptionsMonitor here ...
return false;
}
public override string FormatErrorMessage(string name)
{
// ... use myOptionsMonitor here ...
return string.Empty;
}
}
Unfortunately when I want to use this as an attribute in my DTO
internal class MyDTO
{
[MyValidationAttribute]
public string Foo { get; set; }
}
I get the error message
There is no argument given that corresponds to the required formal
parameter 'myOptionsMonitor' of
'MyValidationAttribute.MyValidationAttribute(IOptionsMonitor)'
Is there a way I can use dependency injection for validation attributes? I know that I can use the ValidationContext like so
internal class MyValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value != null)
{
IOptionsMonitor<MyOptions> myOptionsMonitor = validationContext.GetService<IOptionsMonitor<MyOptions>>();
// ...
return ValidationResult.Success;
}
return new ValidationResult("Something failed");
}
}
But I want to use the FormatErrorMessage method from the base class and this has no access to the options service.
My current solution
For now, this is the code I'm using
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
internal class CustomValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
IOptionsMonitor<MyOptions> myOptionsMonitor = validationContext.GetService<IOptionsMonitor<MyOptions>>();
Dictionary<string, string> myMap = myOptionsMonitor.CurrentValue.MyMap;
string key = value.ToString() ?? string.Empty;
if (myMap.ContainsKey(key))
return ValidationResult.Success;
string[] formattedKeys = myMap.Keys.Select(key => $"'{key}'").ToArray();
string keysText = string.Join(" / ", formattedKeys);
string errorMessage = $"Invalid value. Valid ones are {keysText}";
return new ValidationResult(errorMessage);
}
}
Attributes are not designed for this purpose. But you can use action filters instead.
Let`s make your attribute as simple as it can be, we don't need any validation logic there.
[AttributeUsage(AttributeTargets.Property)]
public class CustomValidationAttribute : Attribute
{ }
For my example I created service that we are going to inject
public class SomeService
{
public bool IsValid(string str)
{
return str == "Valid";
}
}
and a class that we are going to validate
public class ClassToValidate
{
[CustomValidation]
public string ValidStr { get; set; } = "Valid";
[CustomValidation]
public string InvalidStr { get; set; } = "Invalid";
}
Now we can finally create action filter to validate our properties. In the snippet below, we hook into ASP.NET Core pipeline to execute code just before our controller action executes. Here I get action arguments and try to find CustomValidationAttribute on any property. If it is there, grab the value from the property, cast to type (I simply invoke .ToString()) and pass to your service. Based on value that is returned from service, we continue execution or add error to ModelState dictionary.
public class CustomValidationActionFilter : ActionFilterAttribute
{
private readonly SomeService someService;
public CustomValidationActionFilter(SomeService someService)
{
this.someService = someService;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
var actionArguments = context.ActionArguments;
foreach (var actionArgument in actionArguments)
{
var propertiesWithAttributes = actionArgument.Value
.GetType()
.GetProperties()
.Where(x => x.GetCustomAttributes(true).Any(y => y.GetType() == typeof(CustomValidationAttribute)))
.ToList();
foreach (var property in propertiesWithAttributes)
{
var value = property.GetValue(actionArgument.Value).ToString();
if (someService.IsValid(value))
continue;
else
context.ModelState.AddModelError(property.Name, "ModelState is invalid!!!");
}
}
base.OnActionExecuting(context);
}
}
Don't forget to add your filter to the pipeline in Startup.cs!
services.AddMvc(x =>
{
x.Filters.Add(typeof(CustomValidationActionFilter));
});
Update:
If you strictly want to use dependency injection inside attribute, you could use service locator anti-pattern. For that we need to emulate DependencyResolver.Current from ASP.NET MVC
public class CustomValidationAttribute : ValidationAttribute
{
private IServiceProvider serviceProvider;
public CustomValidationAttribute()
{
serviceProvider = AppDependencyResolver.Current.GetService<IServiceProvider>();
}
public override bool IsValid(object value)
{
// scope is required for scoped services
using (var scope = serviceProvider.CreateScope())
{
var service = scope.ServiceProvider.GetService<SomeService>();
return base.IsValid(value);
}
}
}
public class AppDependencyResolver
{
private static AppDependencyResolver _resolver;
public static AppDependencyResolver Current
{
get
{
if (_resolver == null)
throw new Exception("AppDependencyResolver not initialized. You should initialize it in Startup class");
return _resolver;
}
}
public static void Init(IServiceProvider services)
{
_resolver = new AppDependencyResolver(services);
}
private readonly IServiceProvider _serviceProvider;
public object GetService(Type serviceType)
{
return _serviceProvider.GetService(serviceType);
}
public T GetService<T>()
{
return (T)_serviceProvider.GetService(typeof(T));
}
private AppDependencyResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
}
It should be initialized in Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
AppDependencyResolver.Init(app.ApplicationServices);
// other code
}

Encrypt/Decrypt property while writing/reading to c# mongo db

Just going to lay out all the info i have:
In short, I am looking for something exactly (literally) like this but compatible with ASP Core (2.2) and the C# MongoDB Driver (2.7).
This seems like such a common requirement, I am very surprised i can't find anything already built.
Here is what i have so far:
Model:
public class Patient
{
//comes from the client as XXXXXXXXX, RegEx: "([0-9]{9})"
//[MongoEncrypt]
public EncryptedString SocialSecurityNumber { get; set; }
}
Attribute:
[AttributeUsage(AttributeTargets.Property)]
public class MongoEncryptAttribute : BsonSerializerAttribute
{
public MongoEncryptAttribute()
{
SerializerType = typeof(MongoEncryptSerializer);
}
}
Custom Serializer:
public interface IMongoEncryptSerializer : IBsonSerializer<EncryptedString>{ }
public class MongoEncryptSerializer : SerializerBase<EncryptedString>, IMongoEncryptSerializer
{
private readonly string _encryptionKey;
public MongoEncryptSerializer(IConfiguration configuration)
{
_encryptionKey = configuration.GetSection("MongoDb")["EncryptionKey"];
}
public override EncryptedString Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var encryptedString = context.Reader.ReadString();
return AesThenHmac.SimpleDecryptWithPassword(encryptedString, _encryptionKey);
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, EncryptedString value)
{
var encryptedString = AesThenHmac.SimpleEncryptWithPassword(value, _encryptionKey);
context.Writer.WriteString(encryptedString);
}
}
Open Items:
Use DI (vanilla .net core DI) to get the Serializer. thinking of something like BsonSerializer.RegisterSerializer(type,serializer) in a bootstrap method where i can access the service collection and do a GetInstance but then i would need string SocialSecurityNumber to use a custom type (maybe SecureString?)
Went with a custom type,EncryptedString, with implicit string conversion
Use DI in the serializer to get the key (initially from IConfiguration/appsettings.json and then ultimately from Azure KeyVault (whole new can of worms for me)) and the EncryptionProvider
deterministic encryption for searching. AesThenHmac comes from this popular post. I can store and retrieve data back fine in its current implementation. But in order to search for SSNs, I need deterministic encryption which this lib does not provide.
My Solution:
Model:
public class Patient
{
//comes from the client as XXXXXXXXX, RegEx: "([0-9]{9})"
public EncryptedString SocialSecurityNumber { get; set; }
}
Custom Type:
public class EncryptedString
{
private readonly string _value;
public EncryptedString(string value)
{
_value = value;
}
public static implicit operator string(EncryptedString s)
{
return s._value;
}
public static implicit operator EncryptedString(string value)
{
if (value == null)
return null;
return new EncryptedString(value);
}
}
Serializer(using Deterministic Encryption):
public interface IEncryptedStringSerializer : IBsonSerializer<EncryptedString> {}
public class EncryptedStringSerializer : SerializerBase<EncryptedString>, IEncryptedStringSerializer
{
private readonly IDeterministicEncrypter _encrypter;
private readonly string _encryptionKey;
public EncryptedStringSerializer(IConfiguration configuration, IDeterministicEncrypter encrypter)
{
_encrypter = encrypter;
_encryptionKey = configuration.GetSection("MongoDb")["EncryptionKey"];
}
public override EncryptedString Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var encryptedString = context.Reader.ReadString();
return _encrypter.DecryptStringWithPassword(encryptedString, _encryptionKey);
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, EncryptedString value)
{
var encryptedString = _encrypter.EncryptStringWithPassword(value, _encryptionKey);
context.Writer.WriteString(encryptedString);
}
}
Registering the serializer:
collection.AddScoped<IEncryptedStringSerializer, EncryptedStringSerializer>();
//then later...
BsonSerializer.RegisterSerializer<EncryptedString>(sp.GetService<IEncryptedStringSerializer>());

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.

Resolving common routes within core of app using structuremap IoC

We've embarked on the mission of doing a Mvc3 implementation of our framework sites, currently with our existing WebForms implementation.
This task has allowed us to integrate IoC and DI with Structuremap for flexibility.
To give you some background, we have the following project structure:
App.Core <- core class library
App.Mvc <- Mvc class library
App.Mvc.Web <- Mvc3 APP
App.WebForms <- Webforms class library
App.WebForms.Web <- Webforms app or site
We are using Mvc's routing on both the Mvc and the WebForms implementations, we used the same method for route publishing as in the Orchard Project by using a IRouteProvider, where N number of implementation of IRouteProvider can be created to add routes for a publisher to register and order by their priorities.
This is working fine, we are able to register and use the routes with the UrlHelper in MVC or Page.GetRouteUrl in WebForms.
The problem is that we now have a requirement for the App.Core to also be able to resolve these routes (not all of them, but some of the most common ones) and these can change according to the site being implemented.
For example, product detail default route may be "/{category}/{product_name}/{product_id}", but we want the ability to override this and for a certain site have "/{brand}/{product_name}/{product_id}" instead.
This means that in the Core we can't simply just use RouteTable.Routes.GetVirtualPath with a fixed set of parameters, because these might change from site to site.
We created a IRouteResolver interface with methods for the most common routes, which has a default implementation that is registered with SM in each of the class libraries (Mvc or Webforms), but could also be overriden on each site.
The interface looks like:
public interface IRouteResolver
{
string GetRouteUrl(object routeParameters);
string GetRouteUrl(RouteValueDictionary routeParameters);
string GetRouteUrl(string routeName, object routeParameters);
string GetRouteUrl(string routeName, RouteValueDictionary routeParameters);
string GetUrlFor(Product product);
string GetUrlFor(Category category);
string GetUrlFor(Brand brand);
}
The default Mvc implementation of the interface looks like:
public class MvcRouteResolver : IRouteResolver
{
UrlHelper _urlHelper;
ICategoryModelBroker _categoryModelBroker;
IBrandModelBroker _brandModelBroker;
IProductModelBroker _productModelBroker;
public MvcRouteResolver(UrlHelper urlHelper)
{
_urlHelper = urlHelper;
_categoryModelBroker = ObjectFactory.GetInstance<ICategoryModelBroker>();
_brandModelBroker = ObjectFactory.GetInstance<IBrandModelBroker>();
_productModelBroker = ObjectFactory.GetInstance<IProductModelBroker>();
}
public string GetRouteUrl(object routeParameters)
{
return GetRouteUrl(new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(System.Web.Routing.RouteValueDictionary routeParameters)
{
return GetRouteUrl(null, new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(string routeName, object routeParameters)
{
return GetRouteUrl(routeName, new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(string routeName, System.Web.Routing.RouteValueDictionary routeParameters)
{
return _urlHelper.RouteUrl(routeName, routeParameters);
}
public string GetUrlFor(Product product)
{
string category = string.Empty;
if (product.Categories.Count > 0)
category = product.Categories[0].Breadcrumb.Replace("##", "-");
else if (product.Brands.Any())
category = product.Brands.FirstOrDefault().Name;
else
category = "detail";
return GetRouteUrl(new { controller="Product", action="Detail", productId = product.Id, brandName = _productModelBroker.GetSlug(product), productName = _productModelBroker.GetSluggedName(product) });
}
public string GetUrlFor(Category category)
{
return GetRouteUrl(new { controller = "Product", action = "ListByCategory", id = category.Id, name = _categoryModelBroker.GetSlug(category) });
}
public string GetUrlFor(Brand brand)
{
return GetRouteUrl(new { controller = "Product", action = "ListByBrand", id = brand.Id, name = _brandModelBroker.GetSlug(brand) });
}
}
The default WebForms implementation looks like:
public class WebRouteResolver : IRouteResolver
{
Control _control;
HttpContext _context;
public WebRouteResolver()
:this(HttpContext.Current)
{
}
public WebRouteResolver(HttpContext context)
{
_context = context;
}
public WebRouteResolver(Control control)
{
_control = control;
}
public WebRouteResolver(Page page)
{
_control = page as Control;
}
public string GetRouteUrl(object routeParameters)
{
return GetRouteUrl(new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(System.Web.Routing.RouteValueDictionary routeParameters)
{
return GetRouteUrl(null, new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(string routeName, object routeParameters)
{
return GetRouteUrl(routeName, new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(string routeName, System.Web.Routing.RouteValueDictionary routeParameters)
{
VirtualPathData virtualPath = null;
if(_control.IsNotNull())
virtualPath = RouteTable.Routes.GetVirtualPath(_control.Page.Request.RequestContext, routeName, routeParameters);
else
virtualPath = RouteTable.Routes.GetVirtualPath(_context.Request.RequestContext, routeName, routeParameters);
if (virtualPath != null)
{
return virtualPath.VirtualPath;
}
return null;
}
private string ResolveUrl(string originalUrl)
{
if(_control.IsNotNull())
return _control.ResolveUrl(originalUrl);
// *** Absolute path - just return
if (originalUrl.IndexOf("://") != -1)
return originalUrl;
// *** Fix up image path for ~ root app dir directory
if (originalUrl.StartsWith("~"))
{
string newUrl = "";
if (_context != null)
newUrl = _context.Request.ApplicationPath +
originalUrl.Substring(1).Replace("//", "/");
else
// *** Not context: assume current directory is the base directory
throw new ArgumentException("Invalid URL: Relative URL not allowed.");
// *** Just to be sure fix up any double slashes
return newUrl;
}
return originalUrl;
}
public string GetUrlFor(Product product)
{
string category = string.Empty;
if (product.Categories.Count > 0)
category = product.Categories[0].Breadcrumb.Replace("##", "-");
else if (product.Brands.Any())
category = product.Brands.FirstOrDefault().Name;
else
category = "detail";
if (Config.RoutingEnabled)
{
return GetRouteUrl(new { #category = CommonHelper.ToFriendlyUrl(category), name = CommonHelper.ToFriendlyUrl(product.Name), id = product.Id });
}
return ResolveUrl(Config.GetStoreSetting("productDetailUrl")) + "?id={0}&name={1}&category={2}".Fill(product.Id, CommonHelper.ToFriendlyUrl(product.Name), CommonHelper.ToFriendlyUrl(category));
}
public string GetUrlFor(Category category)
{
string breadcrumb = category.Breadcrumb.Replace("##", "-");
if (Config.RoutingEnabled)
return GetRouteUrl(new { #category = CommonHelper.ToFriendlyUrl(breadcrumb), category_id = category.Id});
return ResolveUrl(Config.GetStoreSetting("productListingUrl") + "?category_id={0}&category={1}".Fill(category.Id, CommonHelper.ToFriendlyUrl(category.Name)));
}
public string GetUrlFor(Brand brand)
{
if (Config.RoutingEnabled)
return GetRouteUrl(new { #brand = CommonHelper.ToFriendlyUrl(brand.Name), brand_id = brand.Id });
return ResolveUrl(Config.GetStoreSetting("productListingUrl") + "?brand_id={0}&brand={1}".Fill(brand.Id, CommonHelper.ToFriendlyUrl(brand.Name)));
}
}
Problem now is that because of the constructors arguments (UrlHelper on the Mvc and Page or Control on the Webforms) we are forced to use concrete types instead of using SM to grab the instance of the IRouteResolver plugin.
For example I have the following extensions to make the resolver available on the Page or Control
public static IRouteResolver RouteResolver(this Control control)
{
return new WebRouteResolver(control);
}
public static IRouteResolver RouteResolver(this Page page)
{
return new WebRouteResolver(page);
}
This covers the default behaviour for Web or Mvc but not the case when we want to specifically override the resolver on a per site basis.
Questions are, is it safe to add these constructor arguments as plugins in SM?
Is there another approach/pattern you can recommend for this feature request?
Any ideas/suggestions will be greatly appreciated.
Many thanks,
P.
I think I found the answer in Passing constructor arguments when using StructureMap
As long as the overrides have the same constructors, I think this:
public static IRouteResolver RouteResolver(this Control control)
{
return ObjectFactory.With("control").EqualTo(control).GetInstance<IRouteResolver>();
}
public static IRouteResolver RouteResolver(this Page page)
{
return ObjectFactory.With("page").EqualTo(page).GetInstance<IRouteResolver>();
}
Will probably work, out to do some testing
I've taken another approach, the above did not work on the Core. I now rely only on the RouteCollection and the HttpContext to resolve the routes:
public abstract class BaseRouteResolver : IRouteResolver
{
protected HttpContext _context;
protected RouteCollection _routeCollection;
public BaseRouteResolver()
:this(RouteTable.Routes, HttpContext.Current)
{
}
public BaseRouteResolver(RouteCollection routeCollection, HttpContext context)
{
_routeCollection = routeCollection;
_context = context;
}
public string GetRouteUrl(object routeParameters)
{
return GetRouteUrl(new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(System.Web.Routing.RouteValueDictionary routeParameters)
{
return GetRouteUrl(null, new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(string routeName, object routeParameters)
{
return GetRouteUrl(routeName, new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(string routeName, System.Web.Routing.RouteValueDictionary routeParameters)
{
VirtualPathData virtualPath = _routeCollection.GetVirtualPath(_context.Request.RequestContext, routeName, routeParameters);
if (virtualPath != null)
return virtualPath.VirtualPath;
return null;
}
public abstract string GetUrlFor(Product product);
public abstract string GetUrlFor(Category category);
public abstract string GetUrlFor(Brand brand);
}

Categories

Resources