Can this be done using ASP.NET URL Rewriting - asp.net

There are about 1700 articles listed on my website created using ASP.NET 4.0 Web Forms. These articles have a url format as:
http://www.mymymyarticles.com/Article.aspx?ID=400
I have explored ASP.NET Friendly URLs as well IIS URL Rewrite. These extensions are great but once a rule is created, they treat all url's generically.
Is it possible that I manually generate my own url string for every url that exists on my website? For eg:
I want to permanent redirect http://www.mymymyarticles.com/Article.aspx?ID=400 to http://www.mymymyarticles.com/this-is-a-very-long-url whereas
http://www.mymymyarticles.com/Article.aspx?ID=500 can be redirected to http://www.mymymyarticles.com/article/short-url and
http://www.mymymyarticles.com/Article.aspx?ID=523 can be redirected to http://www.mymymyarticles.com/very-short-url
So you can see there is no uniformity in the url's that I want to manually generate. Basically I want full control over the url's. How can I go about this. Will it affect performance?
Any examples are appreciated.

Do you have a way of mapping an ID to the url of the new page? If that is the case, you could probably achieve this with ASP.NET Routing. What I would do is start with defining a route:
var route = routes.MapRoute(
"LegacyDocument",
"Articles.aspx{*pathInfo}",
null,
constraints: new { pathInfo = new LegacyDocumentRouteConstraint() }
);
route.RouteHandler = new RedirectRouteHandler();
This route merely captures any requests for /articles.aspx, but it has a constraint and a custom route handler.
The purpose of the constraint is to ensure we at least have the ID query string property and it is a number:
public class LegacyDocumentRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.UrlGeneration)
{
// Don't both doing anything while we generate links.
return false;
}
string id = httpContext.Request.QueryString["id"];
if (string.IsNullOrWhiteSpace(id))
{
// No query string argument was provided.
return false;
}
int documentId;
if (!int.TryParse(id, out documentId))
{
// The Id is not a number.
return false;
}
// Set the output document Id in the route values.
values["documentId"] = documentId;
return true;
}
}
If the Id was not provided, or was not a number, we can't match to an existing document, so the route will be skipped over. But when the constraint is satisfied, we store a variable in the route values values["documentId"] = documentId so we can then use it again (without having to parse it from the query string again) later in the route handler:
public class RedirectRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext context)
{
int documentId = (int)context.RouteData.Values["documentId"];
return new RedirectLegacyDocumentHttpHandler(documentId);
}
private class RedirectLegacyDocumentHttpHandler : IHttpHandler
{
private int _documentId;
public RedirectHttpHandler(int documentId)
{
_documentId = documentId;
}
public bool IsReusable { get { return false; } }
public void ProcessRequest(HttpContext context)
{
var response = context.Response;
string url = ResolveNewDocumentUrl();
if (url == null)
{
// Issue a 410 to say the document is no longer available?
response.StatusCode = 410;
}
else
{
// Issue a 301 to the new location.
response.RedirectPermanent(url);
}
}
public string ResolveNewDocumentUrl()
{
// Resolve to a new url using _documentId
}
}
}
Route handlers perform the logic of mapping from ASP.NET routing back into the IHttpHandler logic of the ASP.NET runtime. In normal MVC, this would map to the standard MvcHandler which invokes controllers, but in our case, we need only to issue a redirect.
In the route handler, we grab our document Id from the route values, and create a new HTTP handler which performs the actual redirect. You'll need to plumb in the bit which you resolve what the actual new url would be (ResolveNewDocumentUrl), but generally it will resolve the url, if the url comes back as null, we'll issue a HTTP 410 Gone response to say to clients (and more importantly crawlers) that the item is no longer there, or it will issue an HTTP 301 Permanent Redirect with the appropriate location header to the new url.

I had overcome this by creating an xml file on the server with the below schema
<URLMapper>
<Code>1</Code>
<OLDURL>%Oldurl.aspx%</OLDURL>
<NEWURL>default.aspx</NEWURL>
<PermanentRedirect>true</PermanentRedirect>
<Order>1</Order>
<Status>true</Status>
</URLMapper>
Loaded this in Application_Start Event to an application variable (in the form of a datatable).
And in the Begin Request --
void Application_BeginRequest(object sender, EventArgs e)
{
if (Application["URLMapper"] == null) return;
DataTable dtURLs = Application["URLMapper"] as DataTable;
if (dtURLs.Rows.Count == 0) return;
string OrigUrl = HttpContext.Current.Request.Url.ToString().ToLower().Replace("'", "`");
DataRow[] drFound = dtURLs.Select("Status = true and '" + OrigUrl.Trim() + "' like oldurl", "Order",DataViewRowState.CurrentRows);
if (drFound.Length == 0) return;
string OldURL = drFound[0]["OldURL"].ToString().Replace("%","");
Response.RedirectPermanent(OrigUrl.Replace(OldURL, drFound[0]["NewURL"].ToString().Trim()), true);
return;
}

Related

Kentor Auth Services choose programmaticaly entityId based on page url

I'm integrating our asp.net MVC application with SAML2 Authentication. And using Kentor.AuthServices as module as described at kentor.AuthServices Configuration
Everithing works fine. But next step is to add usage of second service provider (which configured to use another auth mechanisms on server side) only for specified range of pages.
First, how to configure it via web.config to add second SP (not the second IdP in scope of first SP) with different entityId.
And Second, how to switch programmatically to second SP? I assume that it should happend in global.asax file in method Application_BeginRequest, but how?
Using two different SP instances in the same application is a quite rare scenario. But if you are really sure you need it, it can be achieved.
You will have to use the Kentor.AuthServices.Owin package and do the configuration in code - web.config won't do. Register two instances of the middleware. Each one will have their own configuration, including their own SP EntityID. Also make sure to change the ModulePath of at least one of them so that they get different endpoint addresses.
To challenge an authentication from either one, set the right authentication scheme in the challenge (typically in a ChallengeResult returned from a controller)
Self-answering.
Here is a workaround for multiple SP for MVC or HttpModule package, switching is based on specified range of URLs. In my case different SP realize different amount of security factors.
First, implementing custom IOptions and CookieHandler, with ability to switch to correct instance. In the web.config file, two kentor.authServices sections must be defined. In my case only "entityId" attribute differs.
public class CustomOptions : IOptions
{
private IOptions options1Factor;
private IOptions options2Factor;
private Func<bool> _checkIsSecure;
public CustomOptions(Func<bool> checkIsSecure)
{
_checkIsSecure = checkIsSecure;
AddOption(out options2Factor, "kentor.authServices1");
AddOption(out options1Factor, "kentor.authServices");
}
private void AddOption(out IOptions options, string sectionName)
{
var sp = new SPOptions((KentorAuthServicesSection)ConfigurationManager.GetSection(sectionName));
options = new Options(sp);
KentorAuthServicesSection.Current.IdentityProviders.RegisterIdentityProviders(options);
KentorAuthServicesSection.Current.Federations.RegisterFederations(options);
}
public SPOptions SPOptions
{
get
{
if (_checkIsSecure())
return options2Factor.SPOptions;
return options1Factor.SPOptions;
}
}
public IdentityProviderDictionary IdentityProviders
{
get
{
if (_checkIsSecure())
return options2Factor.IdentityProviders;
return options1Factor.IdentityProviders;
}
}
public KentorAuthServicesNotifications Notifications
{
get
{
if (_checkIsSecure())
return options2Factor.Notifications;
return options1Factor.Notifications;
}
}
}
public class CustomCookieHandler : CookieHandler
{
private Func<bool> _checkIsSecure;
private CookieHandler _originalCookieHandler1Factor;
private CookieHandler _originalCookieHandler2Factor;
public CustomCookieHandler(Func<bool> checkIsSecure)
{
_checkIsSecure = checkIsSecure;
_originalCookieHandler1Factor = new ChunkedCookieHandler()
{
Name = "commonAuth",
RequireSsl = false
};
_originalCookieHandler2Factor = new ChunkedCookieHandler()
{
Name = "securedAuth",
RequireSsl = false
};
}
public override string MatchCookiePath(Uri baseUri, Uri targetUri)
{
if (_checkIsSecure())
return _originalCookieHandler2Factor.MatchCookiePath(baseUri, targetUri);
return _originalCookieHandler1Factor.MatchCookiePath(baseUri, targetUri);
}
protected override void DeleteCore(string name, string path, string domain, HttpContext context)
{
if (_checkIsSecure())
_originalCookieHandler2Factor.Delete();
else
_originalCookieHandler1Factor.Delete();
}
protected override byte[] ReadCore(string name, HttpContext context)
{
if (_checkIsSecure())
return _originalCookieHandler2Factor.Read();
return _originalCookieHandler1Factor.Read();
}
protected override void WriteCore(byte[] value, string name, string path, string domain, DateTime expirationTime, bool secure, bool httpOnly, HttpContext context)
{
if (_checkIsSecure())
_originalCookieHandler2Factor.Write(value, true, expirationTime);
else
_originalCookieHandler1Factor.Write(value, true, expirationTime);
}
}
In Global.asax file setting static properties to custom implementations. No more modifications needed.
protected void Application_Start()
{
FederatedAuthentication.FederationConfiguration.CookieHandler = new CustomCookieHandler(CheckIsSecure);
Kentor.AuthServices.Mvc.AuthServicesController.Options = new CustomOptions(CheckIsSecure);
}
private bool CheckIsSecure()
{
if (HttpContext.Current == null)
return false;
var mainHost = "http://host.local"; // host url
var sp = new [] { "/Home/Secure" }; // array of URLs which must be secured with other SP
var request = HttpContext.Current.Request;
var isSecured = sp.Any(x => x.Equals(request.Path, StringComparison.InvariantCultureIgnoreCase));
if (!isSecured && request.Path.Equals("/AuthServices/SignIn", StringComparison.InvariantCultureIgnoreCase))
{
var returnUrl = request.QueryString["ReturnUrl"];
isSecured = !string.IsNullOrEmpty(returnUrl) &&
sp.Any(x => x.Equals(returnUrl, StringComparison.InvariantCultureIgnoreCase));
}
if (!isSecured && request.Path.Equals("/AuthServices/Acs", StringComparison.InvariantCultureIgnoreCase))
{
var _r = new HttpRequestWrapper(request).ToHttpRequestData();
isSecured = _r != null && _r.StoredRequestState != null && _r.StoredRequestState.ReturnUrl != null
&& sp.Any(x => x.Equals(_r.StoredRequestState.ReturnUrl.ToString(),
StringComparison.InvariantCultureIgnoreCase));
}
if (!isSecured && !string.IsNullOrEmpty(request.Headers["Referer"]))
{
var referer = request.Headers["Referer"];
isSecured = sp
.Select(x => string.Format("{0}/{1}", mainHost.TrimEnd('/'), x.TrimStart('/')))
.Any(x => x.Equals(referer, StringComparison.InvariantCultureIgnoreCase));
}
return isSecured;
}

ASP.NET Web Api returns 200 OK when it should return 404

I have the following action methods on a controller in an ASP.NET Web API project:
[Route("api/v2/project/{projectId}/stuff"), HttpGet]
public IHttpActionResult Get(int projectId)
[Route("api/v2/project/{projectId}/stuff/{id:guid}"), HttpGet]
public IHttpActionResult Get(int projectId, [FromUri] Guid id)
[Route("api/v2/project/{projectId}/stuff"), HttpPost]
public IHttpActionResult Post(int projectId, [Required] Stuff stuff)
[Route("api/v2/project/{projectId}/stuff/{id:guid}"), HttpPut]
public IHttpActionResult Put(int projectId, [FromUri] Guid blastId, Stuff stuff)
[Route("api/v2/project/{projectId}/stuff/{id:guid}"), HttpDelete]
public IHttpActionResult Delete(int projectId, [FromUri] Guid id)
Due to a javascript error, I made a DELETE request to
api/v2/project/1234/stuff/undefined
i.e. instead of a GUID for the id, I got the string "undefined". As far as I can tell, this shouldn't match any of my routes, but instead of a 404 Not found (or even 405 Method not allowed), I got a 200 OK as response.
I set a breakpoint in each of these action methods and repeated the request using Fiddler, but none of the breakpoints was hit. I also tried installing the WebApiRouteDebugger package from nuget, but we're using a custom controller factory which hooks things up through our DI container, so I couldn't get it to work at all. I even tried throwing the following exception from one of my globally registered filters:
throw new Exception(actionContext.ControllerContext.ControllerDescriptor.ControllerName +
" " + actionContext.ActionDescriptor.ActionName);
but the DELETE request still goes through to 200 OK (no requests to valid urls seem to do that).
How else can I troubleshoot this? What could be the root cause?
In your Global.asax.cs file where your protected void Application_Start(object sender, EventArgs e) is, add the following:
protected void Application_BeginRequest(object sender, EventArgs e)
{
}
All server requests should come through here.
Add these using if not there.
using System.Web.Compilation;
using System.Reflection;
Then in the begin request call add this code to list out all of the active routes.
string Items = "";
IEnumerable<Assembly> Assemblies = BuildManager.GetReferencedAssemblies().Cast<Assembly>();
foreach (Assembly FoundAssembly in Assemblies)
{
string AssemblyName = FoundAssembly.FullName;
IEnumerable<TypeInfo> Types = FoundAssembly.DefinedTypes.Where(type => type != null && type.IsPublic && type.IsClass && !type.IsAbstract && typeof(ApiController).IsAssignableFrom(type));
foreach (TypeInfo ControllerType in Types)
{
System.Web.Http.Controllers.ApiControllerActionSelector ApiControllerSelection = new System.Web.Http.Controllers.ApiControllerActionSelector();
System.Web.Http.Controllers.HttpControllerDescriptor ApiDescriptor = new System.Web.Http.Controllers.HttpControllerDescriptor(new System.Web.Http.HttpConfiguration(), ControllerType.Name, ControllerType);
ILookup<string, System.Web.Http.Controllers.HttpActionDescriptor> ApiMappings = ApiControllerSelection.GetActionMapping(ApiDescriptor);
foreach (var Maps in ApiMappings)
{
foreach (System.Web.Http.Controllers.HttpActionDescriptor Actions in Maps)
{
Items += "[ controller=" + ControllerType.Name + " action=" + ((System.Web.Http.Controllers.ReflectedHttpActionDescriptor)(Actions)).MethodInfo + "]";
}
}
}
}
This will list all controllers and their signatures. If your URL does not fit into any of these you may have to expand the list to include non controller routes.
I had the same issue. In my startup.cs I had the method
app.Run(async context =>
{
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("My Api");
});
This was causing all routes to receive a 200 response along with the string.
I only wanted to show this response on startup. This was the solution.
app.MapWhen(ctx => ctx.Request.Path.Value == "/", StartupPage);
Along with a method in the same Startup class
private void StartupPage(IAppBuilder app)
{
app.Run(async (context) =>
{
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("My Api");
});
}
I only return the 200 OK message when a request is made on the base url.
I'm am not sure of your routing but I think you have a routing that does not include actions in mapping:
api/v2/project/1234/stuff/undefined
Check web api config for this: api/{controller}[/{action}]/{id}. I think you might hit another route action with your action(like a GET).
P.S. post your route config to update answer.

MVC5 and setting Culture/CultureUI with DropDownList, Cookie, User Profile Setting

I have partially implemented Globalization/Localization in my project. The project requires a database to be used for resource strings and I found an excellent NuGet package called WestWind.Globalization that does exactly what I needed.
This NuGet package allows you to display resource strings using several different methods. It provides an option to generate a strongly typed class that contains all of your resource strings so you can use it like:
#Html.Encode( Resources.lblResourceName )
or
object Value = this.GetLocalResourceObject("ResourceName");
or
object GlobalValue = this.GetGlobalResourceObject("Resources","ResourceKey");
and even:
dbRes.T(resourceName, resourceSet, culture)
I didn't want to specify the culture manually, so I opted for this method:
<p class="pageprompt">#AccountRequestAccount.pagePrompt</p>
For me, Westwind.Globalization is magical. It resolved a huge issue for me, but I ran into a snag that I wasn't sure how to overcome. That was, how to set the Culture/CultureUI so that the package would automatically use a specified language resource.
I created a PartialView that contains a dropdown list of languages. It is contained in the ~/Views/Shared/ folder and gets included in _Layout.cshtml. I coded the GET and POST Controller Actions which work as intended, except that I was unable to persist the Culture/CultureUI settings. I suspect that it was due to a redirect immediately following language selection (explained below)
So, I found an SO question that had an answer that seemed viable. I integrated that answer into my project. The relevant code is:
RouteConfig.cs:
routes.MapRoute("DefaultLocalized",
"{language}-{culture}/{controller}/{action}/{id}",
new
{
controller = "Home",
action = "Index",
id = "",
language = "en",
culture = "US"
});
~/Helpers/InternationalizationAttribute.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Mvc;
namespace GPS_Web_App.Helpers
{
public class InternationalizationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string language =
(string)filterContext.RouteData.Values["language"] ?? "en";
string culture =
(string)filterContext.RouteData.Values["culture"] ?? "US";
Thread.CurrentThread.CurrentCulture =
CultureInfo.GetCultureInfo(string.Format("{0}-{1}",
language, culture));
Thread.CurrentThread.CurrentUICulture =
CultureInfo.GetCultureInfo(string.Format("{0}-{1}",
language, culture));
}
}
}
In my Controllers:
[Authorize]
[Internationalization]
public class AccountController : Controller
{
...
}
So far so good. This works in that I am able to go to a URL of http://example.com/en-mx/Account/Login/ and see the page being localized by Westwind.Globalization and the resource strings I've created.
The problems I have with this are:
If the user is anonymous their language preference should be controlled by cookie (if it exists) otherwise default to en-US.
If the user is authenticated their language preference should be controlled by the Language field in their profile settings. (Simple Membership using ASP.NET Identity 2.0).
There is a language selection dropdown in a global header. The user should be able to choose their language preference from the dropdown and if they do, the setting gets written to cookie (for both anonymous and authenticated users) and if the user is authenticated their Language setting in the user profile gets updated.
Not the end of the world, but it would be highly preferable that the language not be included in the URL. Some might ask, well why did I install #jao's solution? Let me explain that.
All of the code was in place for the dropdown to allow a user to make a language selection. The logic for #1, #2, and #3 above were working correctly, but wouldn't take effect and trigger Westwind.Globalization's DbResourceProvider to pass the selected language resource strings.
What I discovered through debugging was that my settings were not persisting in:
System.Threading.Thread.CurrentThread.CurrentCulture =
System.Globalization.CultureInfo.GetCultureInfo(SelectedLanguage);
System.Threading.Thread.CurrentThread.CurrentUICulture =
System.Globalization.CultureInfo.GetCultureInfo(SelectedLanguage);
Through responses provided by my question here on SO I learned that those settings would not persist/take effect if a redirect was made prior to the original View rendering. Yet a redirect back to the original View seemed sensible since the language was being changed and needed to be rendered again. I think #jao's solution overcomes the redirect problem, but it forces Globalization/Localization to be specified by the URL? Somewhat of a catch-22...
I have asked #jao to review this question and provide any hints on this. I think my question is best summed up as this:
How can I use the user's cookie/profile settings to set the Culture/CultureUI once and for all so that Westwind.Globalization can read Globalization/Localization instead of relying on the Culture being passed in the URL?
I am posting this answer as an alternate, custom way of doing localization with ASP.NET MVC5 with asynchronous controller. Perhaps you may find some gotchas in my solution especially when it comes to routing and setting cookies.
This is sort of a short tutorial I scribbled down for my heterogeneous/custom approach. So I preferred SO over WordPress. :)
Sorry for not giving the precise and discrete answer to your problem. Hopefully it will help you in some other way, and other folks as well; who are looking to do the same sort of setup.
In his blog post, Nadeem Afana described a strategy of creating a separate project Resource in the solution to implement internationalization using static resource files. In the blog sequel, he detailed on extending the same project to handle resources via Databases and XML-driven approaches. For the former one, he used ADO.NET, decoupled from Entity Framework.
We needed to implement both static and dynamic resources within the MVC project, respecting the concepts of MVC conventions.
First lets add a Resources folder in project root, with the desired language variants: ~/Resources/Resources.resx (the default resource file corresponds to en-US culture), ~/Resources/Resources.fi.resx and ~/Resources/Resources.nl.resx. Mark the resources as public, so to make them available in Views.
In ~/Views/Web.config, add the resources namespace under <namespace> element: <add namespace="YourMainNamespace.Reousrces" />. Under controllers, create a base controller class:
Here comes the cookies
namespace YourNamespace.Controllers
{
// Don't forget to inherit other controllers with this
public class BaseController : Controller
{
protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
{
string cultureName = null;
// Attempt to read the culture cookie from Request
HttpCookie cultureCookie = Request.Cookies["_culture"];
if (cultureCookie != null)
cultureName = cultureCookie.Value;
else
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
// Modify current thread's cultures
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
return base.BeginExecuteCore(callback, state);
}
}
}
Next, register a global filter to in ~/Global.asax.cs to ensure that every action should use the correct culture before executing:
Here comes the cookies again!
public class SetCultureActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
var response = filterContext.RequestContext.HttpContext.Response;
var culture = filterContext.RouteData.Values["culture"].ToString();
// Validate input
culture = CultureHelper.GetImplementedCulture(culture);
Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
// Save culture in a cookie
HttpCookie cookie = filterContext.RequestContext.HttpContext.Request.Cookies["_culture"];
if (cookie != null)
cookie.Value = culture; // update cookie value
else
{
cookie = new HttpCookie("_culture");
cookie.Value = culture;
cookie.Expires = DateTime.Now.AddYears(1);
}
response.Cookies.Add(cookie);
}
}
And add GlobalFilters.Filters.Add(new SetCultureActionFilterAttribute()); in MyApplication.Application_Start() method.
In ~/App_Start/RoutesConfig.cs, change the default route to:
routes.MapRoute(
name: "Default",
url: "{culture}/{controller}/{action}/{id}",
defaults: new { culture = "en-US", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
At this point, we would be able to use resources in view. For instance; #Resources.Headline.
Next, we will create a custom attribute called Translatable for model properties.
class TranslatableAttribute : Attribute
{ }
This is enough. But if you want to be able to specify scope, you can use this class to implement it.
Now add a model called Resource with three properties and a helper method:
public class Resource
{
[Key, Column(Order = 0)]
public string Culture { get; set; }
[Key, Column(Order = 1)]
public string Name { get; set; }
public string Value { get; set; }
#region Helpers
// Probably using reflection not the best approach.
public static string GetPropertyValue<T>(string id, string propertyName) where T : class
{
return GetPropertyValue<T>(id, propertyName, Thread.CurrentThread.CurrentUICulture.Name);
}
public static string GetPropertyValue<T>(string id, string propertyName, string culture) where T : class
{
Type entityType = typeof(T);
string[] segments = propertyName.Split('.');
if (segments.Length > 1)
{
entityType = Type.GetType("YourNameSpace.Models." + segments[0]);
propertyName = segments[1];
}
if (entityType == null)
return "?<invalid type>";
var propertyInfo = entityType.GetProperty(propertyName);
var translateableAttribute = propertyInfo.GetCustomAttributes(typeof(TranslatableAttribute), true)
.FirstOrDefault();
/*var requiredAttribute = propertyInfo.GetCustomAttributes(typeof(RequiredAttribute), true)
.FirstOrDefault();*/
if (translateableAttribute == null)
return "?<this field has no translatable attribute>";
var dbCtx = new YourNamespaceDbContext();
var className = entityType.Name;
Resource resource = dbCtx.Resources.Where(r =>
(r.Culture == culture) &&
r.Name == className + id + propertyName).FirstOrDefault();
if (resource != null)
return resource.Value;
//return requiredAttribute == null ? string.Empty : "?<translation not found>";
return string.Empty;
}
#endregion
}
This helper method will help you retrieve the translated content. For instance in view, you can say:
var name = Resource.GetPropertyValue<Product>(item.Id.ToString(), "Name");
Note that, at any point, the data in the translatable field column is unreliable; it will always hold the last updated value. On creating the record, we will mirror all the translatable properties' values in Resource model for all supported cultures.
We are using asynchronous controllers, so for insertion, modification and deletion we will be overriding SaveChangesAsync() in our DbContext class:
public override Task<int> SaveChangesAsync()
{
ObjectContext ctx = ((IObjectContextAdapter)this).ObjectContext;
List<ObjectStateEntry> objectDeletedStateEntryList =
ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted)
.ToList();
List<ObjectStateEntry> objectCreateOrModifiedStateEntryList =
ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Added
| EntityState.Modified)
.ToList();
// First handle the delition case,
// before making changes to entry state
bool changed = UpdateResources(objectDeletedStateEntryList);
// Now save the changes
int result = base.SaveChangesAsync().Result;
// Finally handle the remaining cases
changed |= UpdateResources(objectCreateOrModifiedStateEntryList);
if (changed)
return base.SaveChangesAsync();
return Task.FromResult<int>(result);
}
private bool UpdateResources(List<ObjectStateEntry> objectStateEntryList)
{
bool changed = false;
foreach (ObjectStateEntry entry in objectStateEntryList)
{
var typeName = entry.EntitySet.ElementType.Name;
if (entry.IsRelationship || typeName == "Resource")
return false;
var type = Type.GetType("YourNamespace.Models." + typeName);
if (type == null) // When seeds run (db created for the first-time), sometimes types might not be create
return false;
if (entry.State == EntityState.Deleted)
{
changed |= DeleteResources(type, typeName, entry);
continue;
}
foreach (var propertyInfo in type.GetProperties())
{
var attribute = propertyInfo.GetCustomAttributes(typeof(TranslatableAttribute), true).FirstOrDefault();
if (attribute == null)
continue;
CurrentValueRecord current = entry.CurrentValues;
object idField = current.GetValue(current.GetOrdinal("Id"));
if (idField == null)
continue;
var id = idField.ToString();
var propertyName = propertyInfo.Name;
string newValue = current.GetValue(current.GetOrdinal(propertyName)).ToString();
var name = typeName + id + propertyName;
Resource existingResource = this.Resources.Find(Thread.CurrentThread.CurrentUICulture.Name, name);
if (existingResource == null)
{
foreach (var culture in CultureHelper.Cultures)
{
this.Resources.Add(new Resource
{
Culture = culture,
Name = name,
Value = newValue
});
changed |= true;
}
}
else
{
existingResource.Value = newValue;
changed |= true;
}
}
}
return changed;
}
private bool DeleteResources(Type type, string typeName, ObjectStateEntry entry)
{
bool changed = false;
var firstKey = entry.EntityKey.EntityKeyValues.Where(k => k.Key.Equals("Id", StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
if (firstKey == null)
return false;
var id = firstKey.Value.ToString();
foreach (var propertyInfo in type.GetProperties())
{
var name = typeName + id + propertyInfo.Name;
foreach (var culture in CultureHelper.Cultures)
{
Resource existingResource = this.Resources.Find(culture, name);
if (existingResource == null)
continue;
this.Resources.Remove(existingResource);
changed |= true;
}
}
return changed;
}
This will take care of update and delete.

#Url.Content doesnt resolve absolute path on one server but does on another

We currently have two different servers on same domain. But one server resolves
#Url.Content("~/api/User")'
as
http://domain.com/virtualdirectory/api/User
where as other server doesnt resolve it absolutely; rather it resolves it relatively like
api/user
The code base is same and we are using MVC4. I am not sure as to where we went wrong or if there is any IIS/DNS settings that need to be done in order to get this fixed.
All help is appreciated; thanks :)
This is related with the IIS Rewriting module in your IIS web server that return the path to http://domain.com/virtualdirectory/api/User
Take a look on the part of source code of #Url.Content below:
private static string GenerateClientUrlInternal(HttpContextBase httpContext, string contentPath)
{
if (String.IsNullOrEmpty(contentPath))
{
return contentPath;
}
// can't call VirtualPathUtility.IsAppRelative since it throws on some inputs
bool isAppRelative = contentPath[0] == '~';
if (isAppRelative)
{
string absoluteContentPath = VirtualPathUtility.ToAbsolute(contentPath, httpContext.Request.ApplicationPath);
return GenerateClientUrlInternal(httpContext, absoluteContentPath);
}
// we only want to manipulate the path if URL rewriting is active for this request, else we risk breaking the generated URL
bool wasRequestRewritten = _urlRewriterHelper.WasRequestRewritten(httpContext);
if (!wasRequestRewritten)
{
return contentPath;
}
// Since the rawUrl represents what the user sees in his browser, it is what we want to use as the base
// of our absolute paths. For example, consider mysite.example.com/foo, which is internally
// rewritten to content.example.com/mysite/foo. When we want to generate a link to ~/bar, we want to
// base it from / instead of /foo, otherwise the user ends up seeing mysite.example.com/foo/bar,
// which is incorrect.
string relativeUrlToDestination = MakeRelative(httpContext.Request.Path, contentPath);
string absoluteUrlToDestination = MakeAbsolute(httpContext.Request.RawUrl, relativeUrlToDestination);
return absoluteUrlToDestination;
}
Use the codes below to check whether your web servers are having the URL rewritten:
bool requestWasRewritten = (httpWorkerRequest != null && httpWorkerRequest.GetServerVariable("IIS_WasUrlRewritten") != null);
And Also:
private volatile bool _urlRewriterIsTurnedOnCalculated = false;
private bool _urlRewriterIsTurnedOnValue;
private object _lockObject = new object();
private bool IsUrlRewriterTurnedOn(HttpContextBase httpContext)
{
// Need to do double-check locking because a single instance of this class is shared in the entire app domain (see PathHelpers)
if (!_urlRewriterIsTurnedOnCalculated)
{
lock (_lockObject)
{
if (!_urlRewriterIsTurnedOnCalculated)
{
HttpWorkerRequest httpWorkerRequest = (HttpWorkerRequest)httpContext.GetService(typeof(HttpWorkerRequest));
//bool urlRewriterIsEnabled = (httpWorkerRequest != null && httpWorkerRequest.GetServerVariable(UrlRewriterEnabledServerVar) != null);
bool urlRewriterIsEnabled = (httpWorkerRequest != null && httpWorkerRequest.GetServerVariable("IIS_UrlRewriteModule") != null);
_urlRewriterIsTurnedOnValue = urlRewriterIsEnabled;
_urlRewriterIsTurnedOnCalculated = true;
}
}
}
return _urlRewriterIsTurnedOnValue;
}
In summary, If both requestWasRewritten and IsUrlRewriterTurnedOn
return true, that means one of your web server has IIS Rewrite Module
turned on and running while the other one doesn't have.
For more details on ASP.NET MVC source codes, please refer to this link:
http://aspnetwebstack.codeplex.com/
Hope it helps!

How do I get ASP.NET WebForms Routing to route .asmx JSON calls properly?

I am attempting to implement multi-tenancy in a legacy ASP.NET WebForms app. I want the URL to indicate the proper client, like so:
http://example.com/client_name/Default.aspx
http://example.com/client_name/MyWebService.asmx
However, I cannot get it to route the .asmx's properly. This routing rule picks up all incoming urls just fine:
routes.Add("ClientSelector", new System.Web.Routing.Route
(
"{client}/{*path}",
routeHandler: new ClientRoute()
));
But I am having issues with handling .asmx calls. Here's my IRouteHandler, below. The error I get is:
A first chance exception of type 'System.Web.Services.Protocols.SoapException' occurred in System.Web.Services.dll
Additional information: Unable to handle request without a valid action parameter. Please supply a valid soap action.
It's supposed to be JSON, but for some reason it's not working. I am setting the content-type - if I send this same exact request without routing, it works fine.
public class ClientRoute : System.Web.Routing.IRouteHandler
{
private string m_Path;
private string m_Client;
public ClientRoute() { }
public bool IsReusable { get { return true; } }
public IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
{
this.m_Path = (string)requestContext.RouteData.Values["path"];
this.m_Client = (string)requestContext.RouteData.Values["client"];
string virtualPath = "~/" + this.m_Path;
bool shouldValidate = false;
if (shouldValidate && !UrlAuthorizationModule.CheckUrlAccessForPrincipal(
virtualPath, requestContext.HttpContext.User,
requestContext.HttpContext.Request.HttpMethod))
{
requestContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
requestContext.HttpContext.Response.End();
return null;
}
else
{
HttpContext.Current.RewritePath(virtualPath);
HttpContext.Current.Items.Add("Client", this.m_Client);
if (virtualPath.EndsWith(".aspx"))
return (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(Page));
else
{
var asmxPos = virtualPath.IndexOf(".asmx", StringComparison.OrdinalIgnoreCase);
if (asmxPos >= 0)
{
// What goes here? This isn't working...
var asmxOnlyVirtualPath = virtualPath.Substring(0, asmxPos + 5);
return new System.Web.Services.Protocols.WebServiceHandlerFactory().GetHandler(
HttpContext.Current, HttpContext.Current.Request.HttpMethod, asmxOnlyVirtualPath, HttpContext.Current.Server.MapPath(asmxOnlyVirtualPath));
}
else
return new StaticRoute();
}
}
}
}
Relevant links:
Getting ScriptHandlerFactory handler
The open source http://www.teamlab.com project is built with ASP.NET Webforms, and uses a multitenant/saas model. I noticed you posted another question inquiring about multitenancy.
Perhaps you can look into their code for reference ideas.
I tried my best, ended up failing, and converted all my web services to WCF .svc services instead.

Resources