How can I change all URLs to uppercase / lowercase, or change the default naming convention?
Eg. from:
http://our.umbraco.org/projects/backoffice-extensions/
to:
http://our.umbraco.org/Projects/Backoffice-Extensions/
This is not so hard if you know how to program C#.
You basically need to write your own UrlSegmentProvider (see documentation).
public class UppercaseUrlSegmentProvider: IUrlSegmentProvider
{
private readonly IUrlSegmentProvider provider = new DefaultUrlSegmentProvider();
public string GetUrlSegment(IContentBase content)
{
return this.GetUrlSegment(content, CultureInfo.CurrentCulture);
}
public string GetUrlSegment(IContentBase content, CultureInfo culture)
{
// Maybe you don't want to do that for all contentTypes
// if so, check on the contentType: if (content.ContentTypeId != 1086)
var segment = this.provider.GetUrlSegment(content);
// for the sake of simplicity I have put everything in uppercase,
// you could of course implement something like this:
// http://www.java2s.com/Code/CSharp/Data-Types/CamelCase.htm
return segment.ToUpper().ToUrlSegment();
}
}
To activate your segment provider, you can use the ApplicationStarting method of the ApplicationEventHandler.
public class MySegmentEvents : ApplicationEventHandler
{
protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
base.ApplicationStarting(umbracoApplication, applicationContext);
// UrlSegmentProviderResolver.Current.Clear();
UrlSegmentProviderResolver.Current.InsertType<UppercaseUrlSegmentProvider>(0);
}
}
Attention, if you have implemented the code above, the existing nodes won't change automatically. It's only after a "Save And Publish" that your URL of the particular node will have it's new "segment".
Related
I'm using WebAPI 2.2 and Microsoft.AspNet.OData 5.7.0 to create an OData service that supports paging.
When hosted in the production environment, the WebAPI lives on a server that is not exposed externally, hence the various links returned in the OData response such as the #odata.context and #odata.nextLink point to the internal IP address e.g. http://192.168.X.X/<AccountName>/api/... etc.
I've been able to modify the Request.ODataProperties().NextLink by implementing some logic in each and every ODataController method to replace the internal URL with an external URL like https://account-name.domain.com/api/..., but this is very inconvenient and it only fixes the NextLinks.
Is there some way to set an external host name at configuration time of the OData service? I've seen a property Request.ODataProperties().Path and wonder if it's possible to set a base path at the config.MapODataServiceRoute("odata", "odata", GetModel()); call, or in the GetModel() implementation using for instance the ODataConventionModelBuilder?
UPDATE: The best solution I've come up with so far, is to create a BaseODataController that overrides the Initialize method and checks whether the Request.RequestUri.Host.StartsWith("beginning-of-known-internal-IP-address") and then do a RequestUri rewrite like so:
var externalAddress = ConfigClient.Get().ExternalAddress; // e.g. https://account-name.domain.com
var account = ConfigClient.Get().Id; // e.g. AccountName
var uriToReplace = new Uri(new Uri("http://" + Request.RequestUri.Host), account);
string originalUri = Request.RequestUri.AbsoluteUri;
Request.RequestUri = new Uri(Request.RequestUri.AbsoluteUri.Replace(uriToReplace.AbsoluteUri, externalAddress));
string newUri = Request.RequestUri.AbsoluteUri;
this.GetLogger().Info($"Request URI was rewritten from {originalUri} to {newUri}");
This perfectly fixes the #odata.nextLink URLs for all controllers, but for some reason the #odata.context URLs still get the AccountName part (e.g. https://account-name.domain.com/AccountName/api/odata/$metadata#ControllerName) so they still don't work.
Rewriting the RequestUri is sufficient to affect #odata.nextLink values because the code that computes the next link depends on the RequestUri directly. The other #odata.xxx links are computed via a UrlHelper, which is somehow referencing the path from the original request URI. (Hence the AccountName you see in your #odata.context link. I've seen this behavior in my code, but I haven't been able to track down the source of the cached URI path.)
Rather than rewrite the RequestUri, we can solve the problem by creating a CustomUrlHelper class to rewrite OData links on the fly. The new GetNextPageLink method will handle #odata.nextLink rewrites, and the Link method override will handle all other rewrites.
public class CustomUrlHelper : System.Web.Http.Routing.UrlHelper
{
public CustomUrlHelper(HttpRequestMessage request) : base(request)
{ }
// Change these strings to suit your specific needs.
private static readonly string ODataRouteName = "ODataRoute"; // Must be the same as used in api config
private static readonly string TargetPrefix = "http://localhost:8080/somePathPrefix";
private static readonly int TargetPrefixLength = TargetPrefix.Length;
private static readonly string ReplacementPrefix = "http://www.contoso.com"; // Do not end with slash
// Helper method.
protected string ReplaceTargetPrefix(string link)
{
if (link.StartsWith(TargetPrefix))
{
if (link.Length == TargetPrefixLength)
{
link = ReplacementPrefix;
}
else if (link[TargetPrefixLength] == '/')
{
link = ReplacementPrefix + link.Substring(TargetPrefixLength);
}
}
return link;
}
public override string Link(string routeName, IDictionary<string, object> routeValues)
{
var link = base.Link(routeName, routeValues);
if (routeName == ODataRouteName)
{
link = this.ReplaceTargetPrefix(link);
}
return link;
}
public Uri GetNextPageLink(int pageSize)
{
return new Uri(this.ReplaceTargetPrefix(this.Request.GetNextPageLink(pageSize).ToString()));
}
}
Wire-up the CustomUrlHelper in the Initialize method of a base controller class.
public abstract class BaseODataController : ODataController
{
protected abstract int DefaultPageSize { get; }
protected override void Initialize(System.Web.Http.Controllers.HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
var helper = new CustomUrlHelper(controllerContext.Request);
controllerContext.RequestContext.Url = helper;
controllerContext.Request.ODataProperties().NextLink = helper.GetNextPageLink(this.DefaultPageSize);
}
Note in the above that the page size will be the same for all actions in a given controller class. You can work around this limitation by moving the assignment of ODataProperties().NextLink to the body of a specific action method as follows:
var helper = this.RequestContext.Url as CustomUrlHelper;
this.Request.ODataProperties().NextLink = helper.GetNextPageLink(otherPageSize);
The answer by lencharest is promising, but I found an improvement on his method. Rather than using the UrlHelper, I created a class derived from System.Net.Http.DelegatingHandler. This class is inserted (first) into the message handling pipeline and thus has a crack at altering the incoming HttpRequestMessage. It's an improvement over the above solution because in addition to altering the controller-specific URLs (as the UrlHelper does, e,g, https://data.contoso.com/odata/MyController), it also alters the url that appears as the xml:base in the OData service document (e.g., https://data.contoso.com/odata).
My particular application was to host an OData service behind a proxy server, and I wanted all the URLs presented by the server to be the externally-visible URLs, not the internally-visible ones. And, I didn't want to have to rely on annotations for this; I wanted it to be fully automatic.
The message handler looks like this:
public class BehindProxyMessageHandler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var builder = new UriBuilder(request.RequestUri);
var visibleHost = builder.Host;
var visibleScheme = builder.Scheme;
var visiblePort = builder.Port;
if (request.Headers.Contains("X-Forwarded-Host"))
{
string[] forwardedHosts = request.Headers.GetValues("X-Forwarded-Host").First().Split(new char[] { ',' });
visibleHost = forwardedHosts[0].Trim();
}
if (request.Headers.Contains("X-Forwarded-Proto"))
{
visibleScheme = request.Headers.GetValues("X-Forwarded-Proto").First();
}
if (request.Headers.Contains("X-Forwarded-Port"))
{
try
{
visiblePort = int.Parse(request.Headers.GetValues("X-Forwarded-Port").First());
}
catch (Exception)
{ }
}
builder.Host = visibleHost;
builder.Scheme = visibleScheme;
builder.Port = visiblePort;
request.RequestUri = builder.Uri;
var response = await base.SendAsync(request, cancellationToken);
return response;
}
}
You wire the handler up in WebApiConfig.cs:
config.Routes.MapODataServiceRoute(
routeName: "odata",
routePrefix: "odata",
model: builder.GetEdmModel(),
pathHandler: new DefaultODataPathHandler(),
routingConventions: ODataRoutingConventions.CreateDefault()
);
config.MessageHandlers.Insert(0, new BehindProxyMessageHandler());
There is another solution, but it overrides url for the entire context.
What I'd like to suggest is:
Create owin middleware and override Host and Scheme properties inside
Register the middleware as the first one
Here is an example of middleware
public class RewriteUrlMiddleware : OwinMiddleware
{
public RewriteUrlMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
context.Request.Host = new HostString(Settings.Default.ProxyHost);
context.Request.Scheme = Settings.Default.ProxyScheme;
await Next.Invoke(context);
}
}
ProxyHost is the host you want to have. Example: test.com
ProxyScheme is the scheme you want: Example: https
Example of middleware registration
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Use(typeof(RewriteUrlMiddleware));
var config = new HttpConfiguration();
WebApiConfig.Register(config);
app.UseWebApi(config);
}
}
A couple of years later, using ASP.NET Core, I figured that the easiest way to apply it in my service was to just create a filter that masquerades the host name. (AppConfig is a custom configuration class that contains the host name, among other things.)
public class MasqueradeHostFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var appConfig = context.HttpContext.RequestServices.GetService<AppConfig>();
if (!string.IsNullOrEmpty(appConfig?.MasqueradeHost))
context.HttpContext.Request.Host = new HostString(appConfig.MasqueradeHost);
}
}
Apply the filter to the controller base class.
[MasqueradeHostFilter]
public class AppODataController : ODataController
{
}
The result is a nicely formatted output:
{ "#odata.context":"https://app.example.com/odata/$metadata" }
Just my two cents.
Using system.web.odata 6.0.0.0.
Setting the NextLink property too soon is problematic. Every reply will then have a nextLink in it. The last page should of course be free of such decorations.
http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793048 says:
URLs present in a payload (whether request or response) MAY be
represented as relative URLs.
One way that I hope will work is to override EnableQueryAttribute:
public class myEnableQueryAttribute : EnableQueryAttribute
{
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
var result = base.ApplyQuery(queryable, queryOptions);
var nextlink = queryOptions.Request.ODataProperties().NextLink;
if (nextlink != null)
queryOptions.Request.ODataProperties().NextLink = queryOptions.Request.RequestUri.MakeRelativeUri(nextlink);
return result;
}
}
ApplyQuery() is where the "overflow" is detected. It basically asks for pagesize+1 rows and will set NextLink if the result set contains more than pagesize rows.
At this point it is relatively easy to rewrite NextLink to a relative URL.
The downside is that every odata method must now be adorned with the new myEnableQuery attribute:
[myEnableQuery]
public async Task<IHttpActionResult> Get(ODataQueryOptions<TElement> options)
{
...
}
and other URLs embedded elsewhere remains problematic. odata.context remains a problem. I want to avoid playing with the request URL, because I fail to see how that is maintainable over time.
Your question boils down to controlling the service root URI from within the service itself. My first thought was to look for a hook on the media type formatters used to serialize responses. ODataMediaTypeFormatter.MessageWriterSettings.PayloadBaseUri and ODataMediaTypeFormatter.MessageWriterSettings.ODataUri.ServiceRoot are both settable properties that suggest a solution. Unfortunately, ODataMediaTypeFormatter resets these properties on every call to WriteToStreamAsync.
The work-around is not obvious, but if you dig through the source code you'll eventually reach a call to IODataPathHandler.Link. A path handler is an OData extension point, so you can create a custom path handler that always returns an absolute URI which begins with the service root you desire.
public class CustomPathHandler : DefaultODataPathHandler
{
private const string ServiceRoot = "http://example.com/";
public override string Link(ODataPath path)
{
return ServiceRoot + base.Link(path);
}
}
And then register that path handler during service configuration.
// config is an instance of HttpConfiguration
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: null,
model: builder.GetEdmModel(),
pathHandler: new CustomPathHandler(),
routingConventions: ODataRoutingConventions.CreateDefault()
);
I have a Spring 3.2 Controller with basic request mappings like
#RequestMapping("/action")
public String action(#RequestParam("param") String param) {
//do stuff...
return "view";
}
This controller handles links created by non-technical business users. Sometimes the users mess it up and create links with duplicate parameters, e.g.,
www.example.com/action?param=value¶m=value
The parameter is an exact duplicate and probably a copy/paste error.
My problem is that Spring is concatenating these dupes together, so that the url above will give "value,value" for param, when I want only "value".
What is a good way to detect and handle these duplicates? I know I could change all my #RequestParams to List<String>s and go from there, but that's a whole lot of boilerplate over dozens of request mappings.
Ideally there would be a way to intercept and modify the url parameters before Spring attempts to bind them -- but only for this controller.
I found that I can register a custom String property editor to do this.
class DuplicateParameterReducingPropertyEditor extends PropertyEditorSupport {
Object value;
#Override
public void setValue(Object value) {
if (value instanceof String[]) {
String[] strings = (String[])value;
Set<String> unique = Sets.newHashSet(strings);
this.value = unique.toArray();
} else {
this.value = value;
}
}
#Override
public void setAsText(String text) throws IllegalArgumentException {
this.value = text;
}
#Override
public String getAsText() {
return value.toString();
}
#Override
public Object getValue() {
return value;
}
};
I added this to my controller:
#InitBinder
public void initBinder(WebDataBinder binder) {
PropertyEditor stringEditor = new DuplicateParameterReducingPropertyEditor();
binder.registerCustomEditor(String.class, stringEditor);
}
So whenever Spring encounters a #RequestParam-annotated String method argument, the PropertyEditor is invoked to transform the incoming data if needed. In the case of duplicate parameters, Spring passes a String[] of the values to the property editor setValue, which I can then manipulate.
This does have the results I am looking for. I'm not sure of all the implications of this, though, so I can't endorse it as good solution yet. Not having to alter any handler method signatures is a big plus though.
A good idea would be to extend AbstractNamedValueMethodArgumentResolver with your own strategy. Then the strategy could be used wherever you deem necessary.
This strategy only works for Spring 3.1+ which is not a problem for you since you are using Spring 3.2
I faced the same issue in Spring boot. Eventually I came up with this solution using converter, in case it helps anyone.
This method should be added as part of your WebMvcConfigurer class.
#Override
public void addFormatters(FormatterRegistry registry) {
// Duplicate query parameters converter
registry.addConverter(new Converter<String[], String>() {
public String convert(String[] arr) {
return arr[arr.length - 1]; // Return the last value
}
});
}
How do you get the .resx file the page is currently using? For example, if I set culture to fr-FR on Default.aspx, it should give me Default.aspx.fr.resx or Default.aspx.fr-FR.resx or Default.aspx.resx depends on which one exists.
Do they have something like that in ASP.NET or I have to write it myself?
You can use the GetResourceFileName() method of the ResourceManager class to construct a valid resource culture name. A quick peek at the methods implementation via reflector shows us that the method utilizes the name property of the CultureInfo object passed by the caller to build the resource file name.
protected virtual string GetResourceFileName(CultureInfo culture)
{
StringBuilder builder = new StringBuilder(0xff);
builder.Append(this.BaseNameField);
if (!culture.Equals(CultureInfo.InvariantCulture))
{
CultureInfo.VerifyCultureName(culture, true);
builder.Append('.');
builder.Append(culture.Name);
}
builder.Append(".resources");
return builder.ToString();
}
The GetResourceFileName() method calls the internal static method VerifyCultureName() to ensure we have a valid resource culture name. Taking a look into VerifyCultureName() method shows us that some simple validation takes place.
internal static bool VerifyCultureName(CultureInfo culture, bool throwException)
{
if (culture.m_isInherited)
{
string name = culture.Name;
for (int i = 0; i < name.Length; i++)
{
char c = name[i];
if ((!char.IsLetterOrDigit(c) && (c != '-')) && (c != '_'))
{
if (throwException)
{
throw new ArgumentException(Environment.GetResourceString("Argument_InvalidResourceCultureName", new object[] { name }));
}
return false;
}
}
}
return true;
}
To make use of the GetResourceFileName() method you will have to derive a class from the ResourceManager type and override the virtual method in the base class. The GetResourceFileName() method is protected so we will have to wrap it in a public method to expose it to the outside world.
public class ResxResourceManager : ResourceManager
{
protected override string GetResourceFileName(System.Globalization.CultureInfo culture)
{
return base.GetResourceFileName(culture);
}
public string GetResxFileName(System.Globalization.CultureInfo culture)
{
return GetResourceFileName(culture).Replace(".resources", ".resx");
}
}
I'm not aware of a built-in property that would return the current resource file in this fashion.
CultureInfo.CurrentUICulture.Name returns the current UICulture in use, and the Name property is the shortened form. You could use this to build up the info yourself, for example:
string pageResx = VirtualPathUtility.GetFileName(Request.Path) + "." +
CultureInfo.CurrentUICulture.Name + ".resx";
Depending on what you plan to do with this info I would be somewhat cautious and you should test this approach for your scenario.
I'm looking for a best practice solution that aims to reduce the amount of URLs that are hard-coded in an ASP.NET application.
For example, when viewing a product details screen, performing an edit on these details, and then submitting the changes, the user is redirected back to the product listing screen. Instead of coding the following:
Response.Redirect("~/products/list.aspx?category=books");
I would like to have a solution in place that allows me to do something like this:
Pages.GotoProductList("books");
where Pages is a member of the common base class.
I'm just spit-balling here, and would love to hear any other way in which anyone has managed their application redirects.
EDIT
I ended up creating the following solution: I already had a common base class, to which I added a Pages enum (thanks Mark), with each item having a System.ComponentModel.DescriptionAttribute attribute containing the page's URL:
public enum Pages
{
[Description("~/secure/default.aspx")]
Landing,
[Description("~/secure/modelling/default.aspx")]
ModellingHome,
[Description("~/secure/reports/default.aspx")]
ReportsHome,
[Description("~/error.aspx")]
Error
}
Then I created a few overloaded methods to handle different scenarios. I used reflection to get the URL of the page through it's Description attribute, and I pass query-string parameters as an anonymous type (also using reflection to add each property as a query-string parameter):
private string GetEnumDescription(Enum value)
{
Type type = value.GetType();
string name = Enum.GetName(type, value);
if (name != null)
{
FieldInfo field = type.GetField(name);
if (field != null)
{
DescriptionAttribute attr = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute;
if (attr != null)
return attr.Description;
}
}
return null;
}
protected string GetPageUrl(Enums.Pages target, object variables)
{
var sb = new StringBuilder();
sb.Append(UrlHelper.ResolveUrl(Helper.GetEnumDescription(target)));
if (variables != null)
{
sb.Append("?");
var properties = (variables.GetType()).GetProperties();
foreach (var property in properties)
sb.Append(string.Format("{0}={1}&", property.Name, property.GetValue(variables, null)));
}
return sb.ToString();
}
protected void GotoPage(Enums.Pages target, object variables, bool useTransfer)
{
if(useTransfer)
HttpContext.Current.Server.Transfer(GetPageUrl(target, variables));
else
HttpContext.Current.Response.Redirect(GetPageUrl(target, variables));
}
A typical call would then look like so:
GotoPage(Enums.Pages.Landing, new {id = 12, category = "books"});
Comments?
I'd suggest that you derive your own class ("MyPageClass") from the Page class and include this method there:
public class MyPageClass : Page
{
private const string productListPagePath = "~/products/list.aspx?category=";
protected void GotoProductList(string category)
{
Response.Redirect(productListPagePath + category);
}
}
Then, in your codebehind, make sure that your page derives from this class:
public partial class Default : MyPageClass
{
...
}
within that, you can redirect just by using:
GotoProductList("Books");
Now, this is a bit limited as is since you'll undoubtedly have a variety of other pages like the ProductList page. You could give each one of them its own method in your page class but this is kind of grody and not smoothly extensible.
I solve a problem kind of like this by keeping a db table with a page name/file name mapping in it (I'm calling external, dynamically added HTML files, not ASPX files so my needs are a bit different but I think the principles apply). Your call would then use either a string or, better yet, an enum to redirect:
protected void GoToPage(PageTypeEnum pgType, string category)
{
//Get the enum-to-page mapping from a table or a dictionary object stored in the Application space on startup
Response.Redirect(GetPageString(pgType) + category); // *something* like this
}
From your page your call would be: GoToPage(enumProductList, "Books");
The nice thing is that the call is to a function defined in an ancestor class (no need to pass around or create manager objects) and the path is pretty obvious (intellisense will limit your ranges if you use an enum).
Good luck!
You have a wealth of options availible, and they all start with creating a mapping dictionary, whereas you can reference a keyword to a hard URL. Whether you chose to store it in a configuration file or database lookup table, your options are endless.
You have a huge number of options available here. Database table or XML file are probably the most commonly used examples.
// Please note i have not included any error handling code.
public class RoutingHelper
{
private NameValueCollecton routes;
private void LoadRoutes()
{
//Get your routes from db or config file
routes = /* what ever your source is*/
}
public void RedirectToSection(string section)
{
if(routes == null) LoadRoutes();
Response.Redirect(routes[section]);
}
}
This is just sample code, and it can be implemented any way you wish. The main question you need to think about is where you want to store the mappings. A simple xml file could do it:
`<mappings>
<map name="Books" value="/products.aspx/section=books"/>
...
</mappings>`
and then just load that into your routes collection.
public class BasePage : Page
{
public virtual string GetVirtualUrl()
{
throw new NotImplementedException();
}
public void PageRedirect<T>() where T : BasePage, new()
{
T page = new T();
Response.Redirect(page.GetVirtualUrl());
}
}
public partial class SomePage1 : BasePage
{
protected void Page_Load()
{
// Redirect to SomePage2.aspx
PageRedirect<SomePage2>();
}
}
public partial class SomePage2 : BasePage
{
public override string GetVirtualUrl()
{
return "~/Folder/SomePage2.aspx";
}
}
I'm doing a custom 404 page for a large website that's undergoing a redesign. There are about 40 high-use pages that customers may have bookmarked, and our new site structure will break these bookmarks.
On my custom 404 page, I want to alert them to the new URL if they attempt to navigate to one of these high-use pages via its old URL. So I have a couple of dynamic controls on the 404 page, one for a "did-you-want-this?" type of dialog, and another for a "if-so-go-here (and update your bookmark)" type of dialog. That's the easy part.
To suggest a new URL, I'm looking at the requested URL. If it has key words in it, I'm going to suggest the new URL based on that, and them I'm firing off the appropriate did-you-want..., and if-so... suggestions on the 404 page as mentioned above.
So I want to store these 40-ish key/value pairs (keyword/new-URL pairs) in a data structure, and I'm not sure what would be best. Dictionary? IDictionary? What's the difference and which is more appropriate?
Or am I totally on the wrong track?
Thanks for your time.
I would use the Dictionary<T,T> from the System.Collections.Generic namespace.
You could use NameValueCollection.
Maybe this is overkill for your use case, but I'd probably allow for multiple keywords per Uri, and a relative weight score. Then, dynamically score the keywords that match.
class UriSuggester {
private List<SuggestedUri> Uris { get; set; }
Uri[] GetSuggestions(Uri originalUri) {
var suggestionHits = new Dictionary<SuggestedUri, int>();
foreach (var keyword in KeyWords.Parse(originalUri)) {
// find suggestions matching that keyword
foreach (var suggestedUri in Uris.Where(u => u.Keywords.Contains(keyword)) {
// add a hit for keyword match
suggestionHits[suggestedUri] += 1;
}
}
// order by weight * hits
return suggestionHits.Keys
.OrderBy(s => s.Weight * suggestionHits[s])
.Select(s => s.Uri)
.ToArray();
}
}
class SuggestedUri {
public Uri Suggested { get; set; }
public int Weight { get; set; }
public Keyword[] Keywords;
}
class Keyword {
public string Value { get; set; }
public static Keyword[] Parse(Uri uri);
override Equals;
override GetHashCode;
}
Dictionary would be fine. Wether you store it as the interface type IDictionary or Dictionary itself wouldn't matter much in this case as it's not going to be passed much around, besides on the 404 page itself.
Have you considered doing some URL rewriting to still support the old URLs?
You can consider writing your own class logic and then assign that to a List data structure as following:
public class KeyValuesClass
{
private string a_key;
private string a_value;
public KeyValuesClass(string a_key, string a_value)
{
this.a_key = a_key;
this.a_value = a_value;
}
public string Key
{
get{ return a_key; }
set { a_key = value; }
}
public string Value
{
get{ return a_value; }
set { a_value = value; }
}
}
somewhere in the code
List<KeyValuesClass> my_key_value_list = new List<KeyValuesClass>();
my_key_value_list.Add(new KeyValuesClass("key", "value");
But you can consider Dictionary as our fellow programmer mentioned it above :)