ASP.NET MVC 3 AuthorizeAttribute - asp.net

I am developing a project using ASP.NET MVC 3, now use a MembershipProvider, RoleProvider AuthorizeAttribute and custom. So in certain parts of the code use this:
[Logon(Roles = "login, test1")]
This code works perfectly, for use in the MembershipProvider code:
public override string [] GetRolesForUser (string username)
{
var = UsuarioRepository.GetListaPermissoesByUsuarioEmail permissions (username);
if (permissions == null)
{
nullPermissao var = new string [0];
nullPermissao return;
}
return permissions;
}
My question is. how can I use the following code, which method will need to customize?
I want to check is determined whether a particular type of user who is logged in and if it has certain privileges.
[Logon(Roles = "login, test1," Users = "User1")]
Using override string [] GetRolesForUser (string username) method it checks the Roles, in wich method I can check the User?

This should work out of the box with the AuthorizeAttribute. It checks if HttpContext.User.Identity.Name matches any of the terms you defined under AuthorizeAttribute.Users
As i see from the comments, you rolled your own LogonAttribute where your probably overwrote the OnAuthorize method. This is where the AuthorizeAtrribute does it`s magic.
Original ASP.NET MVC Source
protected virtual bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
throw new ArgumentNullException("httpContext");
IPrincipal user = httpContext.User;
return user.Identity.IsAuthenticated && (this._usersSplit.Length <= 0 || Enumerable.Contains<string>((IEnumerable<string>) this._usersSplit, user.Identity.Name, (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase)) && (this._rolesSplit.Length <= 0 || Enumerable.Any<string>((IEnumerable<string>) this._rolesSplit, new Func<string, bool>(user.IsInRole)));
}
public virtual void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
throw new ArgumentNullException("filterContext");
if (OutputCacheAttribute.IsChildActionCacheActive((ControllerContext) filterContext))
throw new InvalidOperationException(MvcResources.AuthorizeAttribute_CannotUseWithinChildActionCache);
if (this.AuthorizeCore(filterContext.HttpContext))
{
HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0L));
cache.AddValidationCallback(new HttpCacheValidateHandler(this.CacheValidateHandler), (object) null);
}
else
this.HandleUnauthorizedRequest(filterContext);
}

Did you mean to use the following?
[Authorize(Roles = "login, test1", Users = "User1")]

Related

ASP.NET Identity properties extension

I am using Asp.Net Identity for authentication and my requirement is after login currently I can access only User.Identity.Name which is username only.
Is there any way I can add more properties after login like
User.Identity.UserType
User.Identity.DepartmentId
Reason why I wanna use this to avoid Session to identify the user on Views.
First, you need add custom claim to user. For exmaple after creating user:
await _userManager.Value.AddClaimAsync(user, new Claim("UserType", "SomeType"));
Then create an extension method to read claim value:
public static string GetUserType(this IIdentity identity)
{
if (identity == null)
throw new ArgumentNullException(nameof(identity));
var claim = ((ClaimsIdentity)identity).FindFirst("UserType")?.Value;
return claim ?? string.Empty;
}
And you after login you can access user type:
var userType = User.Identity.GetUserType();
And here is a generic extension to get custom claim:
public static T Get<T>(this IIdentity identity, string propertyName)
{
if (identity == null)
throw new ArgumentNullException(nameof(identity));
var value = ((ClaimsIdentity)identity).FindFirst(propertyName)?.Value;
if (value == null)
return default(T);
var type = typeof(T);
if (type.IsEnum)
return (T)Enum.Parse(typeof(T), value);
return (T)Convert.ChangeType(value, typeof(T));
}
For example, you have an enum for UserType:
public enum UserType
{
Admin,
Editor,
User,
}
var userType = User.Identity.Get<UserType>("UserType");
var representativeId = User.Identity.Get<int>("DepartmentId");

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

Adding custom roles to windows roles in ASP.NET MVC 5

I'm building an intranet app using ASP.NET MVC 5.
My goal is to have the authentication of any user made by the Active Directory (i.e. I'm using the "Windows Authentication"), then add groups to any user inside the application (NOT using domain groups).
I've found some very interesting piece of code here:
http://brockallen.com/2013/01/17/adding-custom-roles-to-windows-roles-in-asp-net-using-claims/
But it's not working in my scenario: when I decorate the controller with [Authorize(Role="AppRole")], I can't be authorized even if the user (using Claims) is associated with the "AppRole" role.
This is my code:
In Global.asax.cs
void Application_PostAuthenticateRequest()
{
if (Request.IsAuthenticated)
{
string[] roles = Utils.GetRolesForUser(User.Identity.Name);
var id = ClaimsPrincipal.Current.Identities.First();
foreach (var role in roles)
{
//id.AddClaim(new Claim(ClaimTypes.Role, role.ToString()));
id.AddClaim(new Claim(ClaimTypes.Role, #"Kairos.mil\Compliance"));
}
bool pippo = User.IsInRole("Compliance");
HttpContext.Current.User = (IPrincipal)id ;
bool pippo2 = User.IsInRole("Compliance");
}
}
The function GetRolesForUser is as follows (and is working fine):
public static string[] GetRolesForUser(string username)
{
dbOrdiniPersonaliEntities db = new dbOrdiniPersonaliEntities();
string utente = StripDomain(username);
string[] gruppi = new string[db.vGruppiUtentis.Where(t => t.KairosLogin == utente).Count()];
int i=0;
foreach (var gruppo in db.vGruppiUtentis.Where(t => t.KairosLogin == utente))
{
gruppi[i]=gruppo.GruppoDes;
i=i++;
}
return gruppi;
}
And the controller is decorated with the "standard" Authorize clause:
[Authorize(Roles="AppRole")]
public ActionResult Index(string sortOrder, string currentFilter, string DesSearchString,int? page)
{
// my code here
}
Any idea?
Thanks in advance
UPDATE
Thanks #Leandro
I've tried as you suggested the following code
void Application_PostAuthenticateRequest()
{
if (Request.IsAuthenticated)
{
string[] roles = Utils.GetRolesForUser(User.Identity.Name);
ClaimsIdentity id = ClaimsPrincipal.Current.Identities.First();
foreach (var role in roles)
{
//id.AddClaim(new Claim(ClaimTypes.Role, role.ToString()));
id.AddClaim(new Claim(ClaimTypes.Role, #"Kairos.mil\Compliance"));
}
bool pippo = User.IsInRole("Compliance");
SetPrincipal((IPrincipal)id);
bool pippo2 = User.IsInRole("Compliance");
}
}
But I receive a run-time error when the code reaches this point
SetPrincipal((IPrincipal)id);
The error is as follows
Unable to cast object of type 'System.Security.Principal.WindowsIdentity' to type 'System.Security.Principal.IPrincipal'.
Thanks for your help
UPDATE 2 (maybe solved)
Hi
Looking deeper into SO, I've found this resource
ASP.NET MVC and Windows Authentication with custom roles
Following the answer of #Xhalent, I modified my code as follows
protected void Application_PostAuthenticateRequest()
{
if (Request.IsAuthenticated)
{
String[] roles = Utils.GetRolesForUser(User.Identity.Name);
GenericPrincipal principal = new GenericPrincipal(User.Identity, roles);
Thread.CurrentPrincipal = HttpContext.Current.User = principal;
}
}
It seems now working fine! Any comments? Any drawbacks? Thanks a lot!!
Use this method to save the principal, so it sets up also in the thread:
private void SetPrincipal(IPrincipal principal)
{
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
{
HttpContext.Current.User = principal;
}
}
Update: Also allow anonymous and test if User.IsInRole is getting something inside the method.

How to Authorize user in controller action using list of user from database in ASP.NET MVC 4?

i am doing this in order to authorize user.
[Authorize(Users = #"user1, user2, user3")]
public class MyController : Controller
{
// my stuff
}
i want to do authorization from the list of user which are in database table..
This is how I got it done:
Create a new class (which inherits from AuthorizeAttribute class).
public class CustomAuthorizeAttribute : AuthorizeAttribute
Override the AuthorizeCore method (in CustomAuthorizeAttribute class) and include your custom logic in it.
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
bool isUserAuthorized = false;
// custom logic goes here
// You can get the details of the user making the call using httpContext
// (httpContext.User.Identity.Name)
// Then get the information you have stored on your db, and compare it
// with these details.
// Set isUserAuthorized to true if the values match
return isUserAuthorized;
}
Decorate your controller action method with the attribute that you just created.
[CustomAuthorize]
public ActionResult DoSomething(string something, string someOtherThing)
This link form Gotalove is helpful.
try the following:
"using the link shared by #VikasRana http://www.codeproject.com/Articles/578374/AplusBeginner-splusTutorialplusonplusCustomplusF
I got rid of my enum Role and my method
public CustomAuthorizeAttribute(params object[] roles)
{ ...}
I then changed Role in my model to be a string e.g. User.Role="Admin" instead of int. In my onAuthorization method I changed it to:
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Controller.TempData["ErrorDetails"] = "You must be logged in to access this page";
filterContext.Result = new RedirectResult("~/User/Login");
return;
}
if (filterContext.Result is HttpUnauthorizedResult)
{
filterContext.Controller.TempData["ErrorDetails"] = "You don't have access rights to this page";
filterContext.Result = new RedirectResult("~/User/Login");
return;
}
}
and in my global.asax added this.
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
if (FormsAuthentication.CookiesSupported == true && Request.IsAuthenticated== true)
{
if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
{
try
{
//let us take out the username now
string username = FormsAuthentication.Decrypt(Request.Cookies[FormsAuthentication.FormsCookieName].Value).Name;
string roles = string.Empty;
using (GManagerDBEntities db = new GManagerDBEntities())
{
User user = db.Users.SingleOrDefault(u => u.Username == username);
roles = user.Role;
}
//let us extract the roles from our own custom cookie
//Let us set the Pricipal with our user specific details
HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(
new System.Security.Principal.GenericIdentity(username, "Forms"), roles.Split(';'));
}
catch (Exception)
{
//something went wrong
}
}
}
}
"
Source: Custom user authorization based with roles in asp.net mvc
PS.: In this link, in the same post, there is a second way to fix your problem.
In the bottom of the post.
If this can't to help you, you should try it to.

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.

Resources