How to provide localized validation messages for validation attributes - c#

I am working on an ASP.NET Core application and I would like to override the default validation error messages for data-annotations, like Required, MinLength, MaxLength, etc. I read the documentation at Globalization and localization in ASP.NET Core, and it seems that it does not cover what I was looking for...
For instance, a validation error message for the Required attribute can always be the same for any model property. The default text just states: The {0} field is required, whereby the {0} placeholder will be filled up with the property’s display name.
In my view models, I use the Required attribute without any named arguments, like this...
class ViewModel
{
[Required, MinLength(10)]
public string RequiredProperty { get; set; }
}
Setting an ErrorMessage or ErrorMessageResourceName (and ErrorMessageResourceType) is unnecessary overhead, in my opinion. I thought I could implement something similar to IDisplayMetadataProvider allowing me to return error messages for applied attributes, in case the validation has failed. Is this possible?

For those that end up here, in search of a general solution, the best way to solve it is using a Validation Metadata Provider. I based my solution on this article:
AspNetCore MVC Error Message, I usted the .net framework style localization, and simplified it to use the designed provider.
Add a Resource file for example ValidationsMessages.resx to your project, and set the Access Modifier as Internal or Public, so that the code behind is generated, that will provide you with the ResourceManager static instance.
Add a custom localization for each language ValidationsMessages.es.resx. Remember NOT to set Access Modifier for this files, the code is created on step 1.
Add an implementation of IValidationMetadataProvider
Add the localizations based on the Attributes Type Name like "RequiredAtrribute".
Setup your app on the Startup file.
Sample ValidationsMessages.es.resx
Sample for IValidatioMetadaProvider:
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
public class LocalizedValidationMetadataProvider : IValidationMetadataProvider
{
public LocalizedValidationMetadataProvider()
{
}
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
{
if (context.Key.ModelType.GetTypeInfo().IsValueType && Nullable.GetUnderlyingType(context.Key.ModelType.GetTypeInfo()) == null && context.ValidationMetadata.ValidatorMetadata.Where(m => m.GetType() == typeof(RequiredAttribute)).Count() == 0)
context.ValidationMetadata.ValidatorMetadata.Add(new RequiredAttribute());
foreach (var attribute in context.ValidationMetadata.ValidatorMetadata)
{
var tAttr = attribute as ValidationAttribute;
if (tAttr?.ErrorMessage == null && tAttr?.ErrorMessageResourceName == null)
{
var name = tAttr.GetType().Name;
if (Resources.ValidationsMessages.ResourceManager.GetString(name) != null)
{
tAttr.ErrorMessageResourceType = typeof(Resources.ValidationsMessages);
tAttr.ErrorMessageResourceName = name;
tAttr.ErrorMessage = null;
}
}
}
}
}
Add the provider to the ConfigureServices method on the Startup class:
services.AddMvc(options =>
{
options.ModelMetadataDetailsProviders.Add(new LocalizedValidationMetadataProvider());
})

If you want to change the complete text, you should use resource files to localize it.
Every ValidationAttribute has properties for ErrorMessageResourceType and ErrorMessageResourceName (see source here).
[Required(ErrorMessageResourceName = "BoxLengthRequired", ErrorMessageResourceType = typeof(SharedResource))]
Update
Okay, there seems to be a way to use the localization provider to localize it, but it's still a bit hacky and requires at least one property on the attribute (from this blog post - Word of warning though, it was initially for an old RC1 or RC2 version. It should work, but some of the API in that article may not work):
In startup:
services.AddMvc()
.AddViewLocalization()
.AddDataAnnotationsLocalization();
On your model:
[Required(ErrorMessage = "ViewModelPropertyRequired"), MinLength(10, ErrorMessage = "ViewModelMinLength")]
public string RequiredProperty { get; set; }
and implement/use an localization provider that uses DB (i.e. https://github.com/damienbod/AspNet5Localization).

So, I landed here because of creating my own custom IStringLocalizer and wanted to share my solution because #jlchavez helped me out.
I created a MongoDB IStringLocalizer and wanted to use the resources via the DataAnnotations. Problem is that DataAnnotations Attributes expect localizations via a static class exposing the resources.
One enhancement over jlchavez's answer is that this will fix the resource messages for all ValidationAttribute(s)
services.AddTransient<IValidationMetadataProvider, Models.LocalizedValidationMetadataProvider>();
services.AddOptions<MvcOptions>()
.Configure<IValidationMetadataProvider>((options, provider) =>
{
options.ModelMetadataDetailsProviders.Add(provider);
});
public class Resource
{
public string Id => Culture + "." + Name;
public string Culture { get; set; }
public string Name { get; set; }
public string Text { get; set; }
}
public class MongoLocalizerFactory : IStringLocalizerFactory
{
private readonly IMongoCollection<Resource> _resources;
public MongoLocalizerFactory(IMongoCollection<Resource> resources)
{
_resources = resources;
}
public IStringLocalizer Create(Type resourceSource)
{
return new MongoLocalizer(_resources);
}
public IStringLocalizer Create(string baseName, string location)
{
return new MongoLocalizer(_resources);
}
}
public class MongoLocalizer : IStringLocalizer
{
private readonly IMongoCollection<Resource> _resources;
public MongoLocalizer(IMongoCollection<Resource> resources)
{
_resources = resources;
}
public LocalizedString this[string name]
{
get
{
var value = GetString(name);
return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var format = GetString(name);
var value = string.Format(format ?? name, arguments);
return new LocalizedString(name, value, resourceNotFound: format == null);
}
}
public IStringLocalizer WithCulture(CultureInfo culture)
{
CultureInfo.DefaultThreadCurrentCulture = culture;
return new MongoLocalizer(_resources);
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures)
{
var resources = _resources.Find(r => r.Culture == CultureInfo.CurrentCulture.Parent.Name).ToList();
return resources.Select(r => new LocalizedString(r.Name, r.Text, false));
}
private string GetString(string name)
{
var resource = _resources.Find(r => r.Culture == CultureInfo.CurrentCulture.Parent.Name && r.Name == name).SingleOrDefault();
if (resource != null)
{
return new LocalizedString(resource.Name, resource.Text, false);
}
return new LocalizedString(name, name, true);
}
}
public class LocalizedValidationMetadataProvider : IValidationMetadataProvider
{
private IStringLocalizer _localizer;
public LocalizedValidationMetadataProvider(IStringLocalizer localizer)
{
_localizer = localizer;
}
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
{
foreach(var metadata in context.ValidationMetadata.ValidatorMetadata)
{
if (metadata is ValidationAttribute attribute)
{
attribute.ErrorMessage = _localizer[attribute.ErrorMessage].Value;
}
}
}
}

Thanks for jlchavez's answer, his answer worked for me but I had to make a small correction. In jlchavez's reply there is a message for each validation attribute. But there can also be multiple messages for an attribute so I updated the code as follows:
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
{
if (context.Key.ModelType.GetTypeInfo().IsValueType && Nullable.GetUnderlyingType(context.Key.ModelType.GetTypeInfo()) == null && context.ValidationMetadata.ValidatorMetadata.Where(m => m.GetType() == typeof(RequiredAttribute)).Count() == 0)
context.ValidationMetadata.ValidatorMetadata.Add(new RequiredAttribute());
foreach (var attribute in context.ValidationMetadata.ValidatorMetadata)
{
var tAttr = attribute as ValidationAttribute;
if (tAttr != null && tAttr.ErrorMessage == null && tAttr.ErrorMessageResourceName == null)
{
string defaultErrMessage = tAttr.GetType().BaseType
.GetProperty("ErrorMessageString", BindingFlags.NonPublic | BindingFlags.Instance)
?.GetValue(tAttr)?.ToString();
if (string.IsNullOrEmpty(defaultErrMessage))
continue;
//var name = tAttr.GetType().Name;
if (resourceManager.GetString(defaultErrMessage) != null)
tAttr.ErrorMessage = defaultErrMessage;
}
}
}
With this change, the following setting should also be made:
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
factory.Create(typeof(AppLocales.Modules._Common.ValidationLocale));
})

I encountered the same problem and the solution I used was to create a subclass of the validation attribute to provide the localized error message.
To prevent programmers from accidentally using the non-localized version, I just left out the using statement for the non-localized library.

Related

ASP.NET MVC C# DataTypeAttribute on parameter

I have tried using EmailAddressAttribute on a parameter posted to a controller but it doesn't have the same effect as if it was used within a model.
This is my code:
public void AddEmail(int id, [EmailAddress]string emailAddress)
{
if (!ModelState.IsValid)
throw new Exception();
}
The emailAddress parameter is within the ModelState but it's always valid. However, if I use it within a model like below then it works perfectly fine.
public class TestModel
{
public int Id { get; set; }
[EmailAddress]
public string EmailAddress { get; set; }
}
public void AddEmail(TestModel model)
{
if (!ModelState.IsValid)
throw new Exception();
}
The EmailAddressAttribute class has the AttributeTargets.Parameter so I thought it would work the same.
Can anyone confirm if this is just the way it is? Or is there a way to get it to work the same as the model does?
EDIT: I am using .NET Framework 4.6.2.
Thanks
I don't know if you can use DataTypeAttributes as parameters in a function.
But as an easy way to just check if it is a valid email notation you could use this code:
try {
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch {
return false;
}
EDIT 1:
As mentioned from Mark Vincze here on his blog, you could create a new ActionFilterAttribute like this when you want to have attributes in your action parameters.
public class ValidateActionParametersAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (descriptor != null)
{
var parameters = descriptor.MethodInfo.GetParameters();
foreach (var parameter in parameters)
{
var argument = context.ActionArguments[parameter.Name];
EvaluateValidationAttributes(parameter, argument, context.ModelState);
}
}
base.OnActionExecuting(context);
}
private void EvaluateValidationAttributes(ParameterInfo parameter, object argument, ModelStateDictionary modelState)
{
var validationAttributes = parameter.CustomAttributes;
foreach (var attributeData in validationAttributes)
{
var attributeInstance = CustomAttributeExtensions.GetCustomAttribute(parameter, attributeData.AttributeType);
var validationAttribute = attributeInstance as ValidationAttribute;
if (validationAttribute != null)
{
var isValid = validationAttribute.IsValid(argument);
if (!isValid)
{
modelState.AddModelError(parameter.Name, validationAttribute.FormatErrorMessage(parameter.Name));
}
}
}
}
}
But this also just workes for Actions. Because the ModelState-Class was created to make it easier to check if an incoming binding is valid or not and not just to validate random objects. Here is more about that.
So in your case when AddEmail is a 'normal' method and not an Action you should not use this. In this case, use another validation method such as my first answer.
And if you want to read even more about validation, take a look at this blog post from Brad Wilson.

Selectively Whitelisting Model Fields to Bind

(I realize this question is very similar to How to whitelist/blacklist child object fields in the ModelBinder/UpdateModel method? but my situation is slightly different and there may be a better solution available now that wasn't then.)
Our company sells web-based software that is extremely configurable by the end-user. The nature of this flexibility means that we must do a number of things at run time that would normally be done at compile time.
There are some rather complex rules regarding who has read or read/write access to most everything.
For instance, take this model that we would like to create:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace j6.Business.Site.Models
{
public class ModelBindModel
{
[Required]
[Whitelist(ReadAccess = true, WriteAccess = true)]
public string FirstName { get; set; }
[Whitelist(ReadAccess = true, WriteAccess = true)]
public string MiddleName { get; set; }
[Required]
[Whitelist(ReadAccess = true, WriteAccess = true)]
public string LastName { get; set; }
[Required]
[Whitelist(ReadAccess = User.CanReadSalary, WriteAccess = User.CanWriteSalary)]
public string Salary { get; set; }
[Required]
[Whitelist(ReadAccess = User.CanReadSsn, WriteAccess = User.CanWriteSsn)]
public string Ssn { get; set; }
[Required]
public string SirNotAppearingOnThisPage { get; set; }
}
}
In the controller, it is not difficult to "unbind" things manually.
var resetValue = null;
modelState.Remove(field);
pi = model.GetType().GetProperty(field);
if (pi == null)
{
throw new Exception("An exception occured in ModelHelper.RemoveUnwanted. Field " +
field +
" does not exist in the model " + model.GetType().FullName);
}
// Set the default value.
pi.SetValue(model, resetValue, null);
Using HTML helpers, I can easily access the model metadata and suppress rendering of any fields the user does not have access to.
The kicker: I can't figure out how to access the model metadata anywhere in the CONTROLLER itself to prevent over-posting.
Note that using [Bind(Include...)] is not a functional solution, at least not without additional support. The properties to Include are run-time (not compile time) dependent, and excluding the property does not remove it from the validation.
ViewData.Model is null
ViewData.ModelMetaData is null
[AllowAnonymous]
[HttpPost]
// [Bind(Exclude = "Dummy1" + ",Dummy2")]
public ViewResult Index(ModelBindModel dto)
{
zzz.ModelHelper.RemoveUnwanted(ModelState, dto, new string[] {"Salary", "Ssn"});
ViewBag.Method = "Post";
if (!ModelState.IsValid)
{
return View(dto);
}
return View(dto);
}
Any suggestions on how to access the Model MetaData from the controller? Or a better way to whitelist properties at run time?
Update:
I borrowed a page from this rather excellent resource:
http://www.dotnetcurry.com/ShowArticle.aspx?ID=687
With a model that looks like this:
[Required]
[WhiteList(ReadAccessRule = "Nope", WriteAccessRule = "Nope")]
public string FirstName { get; set; }
[Required]
[WhiteList(ReadAccessRule = "Database.CanRead.Key", WriteAccessRule = "Database.CanWrite.Key")]
public string LastName { get; set; }
The class:
public class WhiteList : Attribute
{
public string ReadAccessRule { get; set; }
public string WriteAccessRule { get; set; }
public Dictionary<string, object> OptionalAttributes()
{
var options = new Dictionary<string, object>();
var canRead = false;
if (ReadAccessRule != "")
{
options.Add("readaccessrule", ReadAccessRule);
}
if (WriteAccessRule != "")
{
options.Add("writeaccessrule", WriteAccessRule);
}
if (ReadAccessRule == "Database.CanRead.Key")
{
canRead = true;
}
options.Add("canread", canRead);
options.Add("always", "be there");
return options;
}
}
And adding these lines to the MetadataProvider class mentioned in the link:
var whiteListValues = attributes.OfType<WhiteList>().FirstOrDefault();
if (whiteListValues != null)
{
metadata.AdditionalValues.Add("WhiteList", whiteListValues.OptionalAttributes());
}
Finally, the heart of the system:
public static void DemandFieldAuthorization<T>(ModelStateDictionary modelState, T model)
{
var metaData = ModelMetadataProviders
.Current
.GetMetadataForType(null, model.GetType());
var props = model.GetType().GetProperties();
foreach (var p in metaData.Properties)
{
if (p.AdditionalValues.ContainsKey("WhiteList"))
{
var whiteListDictionary = (Dictionary<string, object>) p.AdditionalValues["WhiteList"];
var key = "canread";
if (whiteListDictionary.ContainsKey(key))
{
var value = (bool) whiteListDictionary[key];
if (!value)
{
RemoveUnwanted(modelState, model, p.PropertyName);
}
}
}
}
}
To recap my interpretation of your question:
Field access is dynamic; some users may be able to write to a field and some may not.
You have a solution to control this in the view.
You want to prevent a malicious form submission from sending restricted properties, which the model binder will then assign to your model.
Perhaps something like this?
// control general access to the method with attributes
[HttpPost, SomeOtherAttributes]
public ViewResult Edit( Foo model ){
// presumably, you must know the user to apply permissions?
DemandFieldAuthorization( model, user );
// if the prior call didn't throw, continue as usual
if (!ModelState.IsValid){
return View(dto);
}
return View(dto);
}
private void DemandFieldAuthorization<T>( T model, User user ){
// read the model's property metadata
// check the user's permissions
// check the actual POST message
// throw if unauthorized
}
I wrote an extension method a year or so ago that has stood me in good stead a couple of times since. I hope this is of some help, despite not being perhaps the full solution for you. It essentially only allows validation on the fields that have been present on the form sent to the controller:
internal static void ValidateOnlyIncomingFields(this ModelStateDictionary modelStateDictionary, FormCollection formCollection)
{
IEnumerable<string> keysWithNoIncomingValue = null;
IValueProvider valueProvider = null;
try
{
// Transform into a value provider for linq/iteration.
valueProvider = formCollection.ToValueProvider();
// Get all validation keys from the model that haven't just been on screen...
keysWithNoIncomingValue = modelStateDictionary.Keys.Where(keyString => !valueProvider.ContainsPrefix(keyString));
// ...and clear them.
foreach (string errorKey in keysWithNoIncomingValue)
modelStateDictionary[errorKey].Errors.Clear();
}
catch (Exception exception)
{
Functions.LogError(exception);
}
}
Usage:
ModelState.ValidateOnlyIncomingFields(formCollection);
And you'll need a FormCollection parameter on your ActionResult declaration, of course:
public ActionResult MyAction (FormCollection formCollection) {

C# Looking for pattern ideas - Inheritance w/ constructor issue

I have a multiple layered application I'm rewriting using Entity Framework 4 w/ Code First. The important things:
In the data layer, on my context, I have:
public DbSet<MobileSerialContainer> Mobiles { get; set; }
This context has a static instance. I know, I know, terrible practice. There are reasons which aren't relevant to this post as to why I'm doing this.
MobileSerialContainer consists of the following:
[Table("Mobiles")]
public sealed class MobileSerialContainer
{
[Key]
public long Serial { get; set; }
[StringLength(32)]
public string Name { get; set; }
public MobileSerialContainer() { }
public MobileSerialContainer(Mobile mobile)
{
Mobile = mobile;
LeContext.Instance.Mobiles.Add(this);
}
[StringLength(1024)]
public string FullClassName
{
get { return Mobile == null ? "" : Mobile.GetType().AssemblyQualifiedName; }
set
{
if (string.IsNullOrEmpty(value) || value == FullClassName)
return;
Mobile = null;
var type = Type.GetType(value);
if (type == null)
return;
if (!type.IsSubclassOf(typeof(Mobile))
&& type != typeof(Mobile))
return;
var constructor = type.GetConstructor(new [] { GetType() });
// The problem here is that Person ( which extends mobile ) does not have a constructor that takes a MobileSerialContainer.
// This is a problem of course, because I want to make this entire layer transparent to the system, so that each derivative
// of Mobile does not have to implement this second constructor. Blasphemy!
if (constructor == null)
return;
Mobile = (Mobile)constructor.Invoke(new object[] { this });
}
}
public string SerializedString
{
get
{
return Mobile == null ? "" : Mobile.Serialize();
}
set
{
if (Mobile == null)
return;
if (string.IsNullOrEmpty(value))
return;
Mobile.Deserialize(value);
}
}
[NotMapped]
public Mobile Mobile { get; set; }
public void Delete()
{
LeContext.Instance.Mobiles.Remove(this);
}
}
Now... I know this is a long post bear with me. Mobile is this:
public class Mobile
{
public long Serial { get { return Container.Serial; } }
public string Name { get { return Container.Name; } set { Container.Name = value; } }
public Mobile()
{
Container = new MobileSerialContainer(this);
}
public Mobile(MobileSerialContainer container)
{
Container = container;
}
public void Delete()
{
Container.Delete();
}
private MobileSerialContainer Container { get; set; }
protected static string MakeSafeString(string value)
{
if (string.IsNullOrEmpty(value))
return value;
return value.Replace("&", "&")
.Replace(",", "&comma;")
.Replace("=", "&eq;");
}
protected static string MakeUnsafeString(string value)
{
if (string.IsNullOrEmpty(value))
return value;
return value.Replace("&eq;", "=")
.Replace("&comma;", ",")
.Replace("&", "&");
}
public virtual string Serialize()
{
string result = "";
var properties = PersistentProperties;
foreach (var property in properties)
{
string name = MakeSafeString(property.Name);
var value = property.GetValue(this, null);
string unsafeValueString = (string)Convert.ChangeType(value, typeof(string));
string valueString = MakeSafeString(unsafeValueString);
result += name + "=" + valueString + ",";
}
return result;
}
public virtual void Deserialize(string serialized)
{
var properties = PersistentProperties.ToList();
var entries = serialized.Split(',');
foreach (var entry in entries)
{
if (string.IsNullOrEmpty(entry))
continue;
var keyPair = entry.Split('=');
if (keyPair.Length != 2)
continue;
string name = MakeUnsafeString(keyPair[0]);
string value = MakeUnsafeString(keyPair[1]);
var property = properties.FirstOrDefault(p => p.Name == name);
if (property == null)
continue;
object rawValue = Convert.ChangeType(value, property.PropertyType);
property.SetValue(this, rawValue, null);
}
}
protected IEnumerable<PropertyInfo> PersistentProperties
{
get
{
var type = GetType();
var properties = type.GetProperties().Where(p => p.GetCustomAttributes(typeof(PersistAttribute), true).Any());
return properties;
}
}
}
Several layers above this, I have the System layer, in which I have the class Person:
public class Person : Mobile
{
[Persist]
public string LastName { get; set; }
}
The basic idea is this: I want the System layer to have almost no knowledge of the Data layer. It creates anything that extends "Mobile", which is automatically saved to the database. I don't want to have a table for Person, hence the weird serialization stuff, because there are literally hundreds of classes that extend Mobile. I don't want hundreds of tables. All of this serialization stuff works perfectly, the SerializedString bit, saving everything, reloading, etc etc. The only thing I haven't come up with a solution for is:
I don't want to have to implement the two constructors to Person:
public Person() : base() { }
public Person(MobileSerialContainer container)
: base(container) { }
as that requires the System layer to have more knowledge of the Data layer.
The weird serialization string thing stays. The reflection business stays. I know it's slow, but database writes and reads are very rare, and asynchronous anyway.
Besides that, I'm looking for any cool ideas about how to resolve this. Thanks!
[edit]
Changed a miswritten line of code in the MobileSerialContainer class pasted here.
If you are rewriting your application, you could reconsider all the design of your system to keep your domain layer (System layer) independent from your Data Access layer using :
A repository pattern to handle access to your database (dataContext)
A domain layer for your business objects (mobile and stuff)
Inversion Of Control pattern (IOC) to keep your layers loosely coupled
The inheritance stuff is definitively not the good way to go to keep a system loosely coupled.
What you want is type.GetConstructors() not type.GetConstructor(), this will let you get the base constructors and pass the type you are looking for.

AutoMapper Map to different type based on an enum?

I'm starting to implement AutoMapper, first I managed to integrate it with Castle.Windsor, which I'm already using. Now I have a Post entity which I want to map to either a LinkPostModel or an ImagePostModel. Both inherit from PostModel
1) This is what I have so far:
public class PostModelFromPostEntityConverter : ITypeConverter<Post, PostModel>
{
private readonly IPostService postService;
public PostModelFromPostEntityConverter(IPostService postService)
{
if (postService == null)
{
throw new ArgumentNullException("postService");
}
this.postService = postService;
}
public PostModel Convert(ResolutionContext context)
{
Post post = (Post)context.SourceValue;
Link link = post.Link;
if (link.Type == LinkType.Html)
{
return new LinkPostModel
{
Description = link.Description,
PictureUrl = link.Picture,
PostId = post.Id,
PostSlug = postService.GetTitleSlug(post),
Timestamp = post.Created,
Title = link.Title,
UserMessage = post.UserMessage,
UserDisplayName = post.User.DisplayName
};
}
else if (link.Type == LinkType.Image)
{
return new ImagePostModel
{
PictureUrl = link.Picture,
PostId = post.Id,
PostSlug = postService.GetTitleSlug(post),
Timestamp = post.Created,
UserMessage = post.UserMessage,
UserDisplayName = post.User.DisplayName
};
}
return null;
}
}
Obviously the point in implementing AutoMapper is removing repeat code like this, so how am I supposed to map the common stuff, before adding my custom rules (such as the if-clause)
Ideally I'd want this to be something like:
public class PostModelFromPostEntityConverter : ITypeConverter<Post, PostModel>
{
[...]
public PostModel Convert(ResolutionContext context)
{
Post post = (Post)context.SourceValue;
Link link = post.Link;
if (link.Type == LinkType.Html)
{
return Mapper.Map<Post, LinkPostModel>(post);
// and a few ForMember calls?
}
else if (link.Type == LinkType.Image)
{
return Mapper.Map<Post, ImagePostModel>(post);
// and a few ForMember calls?
}
return null;
}
}
2) After this mapping is complete. I have a "parent" mapping, where I need to map an IEnumerable<Post> the following model:
public class PostListModel : IHasOpenGraphMetadata
{
public OpenGraphModel OpenGraph { get; set; } // og:model just describes the latest post
public IList<PostModel> Posts { get; set; }
}
So basically I'd need another TypeConverter (right?), which allows me to map the posts list first, and then create the og:model
I have this, but it feels kind of clunky, I feel it could be better:
public class PostListModelFromPostEntityEnumerableConverter : ITypeConverter<IEnumerable<Post>, PostListModel>
{
public PostListModel Convert(ResolutionContext context)
{
IEnumerable<Post> posts = (IEnumerable<Post>)context.SourceValue;
PostListModel result = new PostListModel
{
Posts = posts.Select(Mapper.Map<Post, PostModel>).ToList()
};
Post first = posts.FirstOrDefault();
result.OpenGraph = Mapper.Map<Post, OpenGraphModel>(first);
return result;
}
}
3) I didn't actually run the code yet, so another question comes to mind, and that is why aren't mappings strongly typed in converters?
IEnumerable<Post> posts = (IEnumerable<Post>)context.SourceValue;
where it could actually be
IEnumerable<Post> posts = context.SourceValue;
Trying to get Necromancer badge.
Nowadays this task can be solved much easier with using ConstructUsing function specifc fields should be filled in the provided action, but all the common fields will go to ForMember execution of the mapping. Collections in this case doesn't requires any additional logic/mapping configurations. Classes that has a property of type collection as well.
cfg.CreateMap<Post, PostModel>()
.ConstructUsing(p =>
{
switch (p.Type)
{
case LinkType.Html: return new LinkPostModel
{
Title = p.Description
// other specific fields
};
case LinkType.Image: return new ImagePostModel
{
// other specific fields
};
}
return null;
})
.ForMember(x => x.PostId, m => m.MapFrom(p => p.Id));
cfg.CreateMap<PostList, PostListModel>();

C# attribute text from resource file?

I have an attribute and i want to load text to the attribute from a resource file.
[IntegerValidation(1, 70, ErrorMessage = Data.Messages.Speed)]
private int i_Speed;
But I keep getting
"An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type"
It works perfectly if i add a string instead of Data.Messages.Text, like:
[IntegerValidation(1, 70, ErrorMessage = "Invalid max speed")]
Any ideas?
Here is my solution. I've added resourceName and resourceType properties to attribute, like microsoft has done in DataAnnotations.
public class CustomAttribute : Attribute
{
public CustomAttribute(Type resourceType, string resourceName)
{
Message = ResourceHelper.GetResourceLookup(resourceType, resourceName);
}
public string Message { get; set; }
}
public class ResourceHelper
{
public static string GetResourceLookup(Type resourceType, string resourceName)
{
if ((resourceType != null) && (resourceName != null))
{
PropertyInfo property = resourceType.GetProperty(resourceName, BindingFlags.Public | BindingFlags.Static);
if (property == null)
{
throw new InvalidOperationException(string.Format("Resource Type Does Not Have Property"));
}
if (property.PropertyType != typeof(string))
{
throw new InvalidOperationException(string.Format("Resource Property is Not String Type"));
}
return (string)property.GetValue(null, null);
}
return null;
}
}
Attribute values are hard-coded into the assembly when you compile. If you want to do anything at execution time, you'll need to use a constant as the key, then put some code into the attribute class itself to load the resource.
Here is the modified version of the one I put together:
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false)]
public class ProviderIconAttribute : Attribute
{
public Image ProviderIcon { get; protected set; }
public ProviderIconAttribute(Type resourceType, string resourceName)
{
var value = ResourceHelper.GetResourceLookup<Image>(resourceType, resourceName);
this.ProviderIcon = value;
}
}
//From http://stackoverflow.com/questions/1150874/c-sharp-attribute-text-from-resource-file
//Only thing I changed was adding NonPublic to binding flags since our images come from other dll's
// and making it generic, as the original only supports strings
public class ResourceHelper
{
public static T GetResourceLookup<T>(Type resourceType, string resourceName)
{
if ((resourceType != null) && (resourceName != null))
{
PropertyInfo property = resourceType.GetProperty(resourceName, BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic);
if (property == null)
{
return default(T);
}
return (T)property.GetValue(null, null);
}
return default(T);
}
}
I came across this problem with the display name for attribute, and I made the following changes:
For our resource file I changed the custom tool property to PublicResXFileCodeGenerator
Then added this to the attribute:
[Display(Name = "MyResourceName", ResourceType = typeof(Resources.MyResources))]
If you're using .NET 3.5 or newer you can use ErrorMessageResourceName and ErrorMessageResourceType parameters.
For example
[Required(ErrorMessageResourceName ="attribute_name" , ErrorMessageResourceType = typeof(resource_file_type))]
Use a string which is the name of the resource. .NET does this with some internal attributes.
The nature of attributes is such that the data you put in attribute properties must be constants. These values will be stored within an assembly, but will never result in compiled code that is executed. Thus you cannot have attribute values that rely on being executed in order to calculate the results.
I have a similar case, where I need to put resource strings into attributes. In C# 6, we have the nameof() capability, and that seems to do the trick.
In my case, I can use [SomeAttribute(nameof(Resources.SomeResourceKey))] and it compiles fine. Then I just have to do a little work on the other end to use that value to get the correct string from the Resources file.
In your case, you might try:
[IntegerValidation(1, 70, ErrorMessageResourceKey = nameof(Data.Messages.Speed))]
private int i_Speed;
Then you can do something along the lines of (pseudo code):
Properties.Resources.ResourceManager.GetString(attribute.ErrorMessageResourceKey);
Here's something I wrote since I couldn't find anything else that does this.:
Input
Write a constant string class in project A.
[GenerateResource]
public static class ResourceFileName
{
public static class ThisSupports
{
public static class NestedClasses
{
[Comment("Comment value")]
public const string ResourceKey = "Resource Value";
}
}
}
Output
And a resource will be generated in the project that contains the constants class.
All you need to do is have this code somewhere:
Source
public class CommentAttribute : Attribute
{
public CommentAttribute(string comment)
{
this.Comment = comment;
}
public string Comment { get; set; }
}
public class GenerateResourceAttribute : Attribute
{
public string FileName { get; set; }
}
public class ResourceGenerator
{
public ResourceGenerator(IEnumerable<Assembly> assemblies)
{
// Loop over the provided assemblies.
foreach (var assembly in assemblies)
{
// Loop over each type in the assembly.
foreach (var type in assembly.GetTypes())
{
// See if the type has the GenerateResource attribute.
var attribute = type.GetCustomAttribute<GenerateResourceAttribute>(false);
if (attribute != null)
{
// If so determine the output directory. First assume it's the current directory.
var outputDirectory = Directory.GetCurrentDirectory();
// Is this assembly part of the output directory?
var index = outputDirectory.LastIndexOf(typeof(ResourceGenerator).Assembly.GetName().Name);
if (index >= 0)
{
// If so remove it and anything after it.
outputDirectory = outputDirectory.Substring(0, index);
// Is the concatenation of the output directory and the target assembly name not a directory?
outputDirectory = Path.Combine(outputDirectory, type.Assembly.GetName().Name);
if (!Directory.Exists(outputDirectory))
{
// If that is the case make it the current directory.
outputDirectory = Directory.GetCurrentDirectory();
}
}
// Use the default file name (Type + "Resources") if one was not provided.
var fileName = attribute.FileName;
if (fileName == null)
{
fileName = type.Name + "Resources";
}
// Add .resx to the end of the file name.
fileName = Path.Combine(outputDirectory, fileName);
if (!fileName.EndsWith(".resx", StringComparison.InvariantCultureIgnoreCase))
{
fileName += ".resx";
}
using (var resx = new ResXResourceWriter(fileName))
{
var tuples = this.GetTuplesRecursive("", type).OrderBy(t => t.Item1);
foreach (var tuple in tuples)
{
var key = tuple.Item1 + tuple.Item2.Name;
var value = tuple.Item2.GetValue(null);
string comment = null;
var commentAttribute = tuple.Item2.GetCustomAttribute<CommentAttribute>();
if (commentAttribute != null)
{
comment = commentAttribute.Comment;
}
resx.AddResource(new ResXDataNode(key, value) { Comment = comment });
}
}
}
}
}
}
private IEnumerable<Tuple<string, FieldInfo>> GetTuplesRecursive(string prefix, Type type)
{
// Get the properties for the current type.
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static))
{
yield return new Tuple<string, FieldInfo>(prefix, field);
}
// Get the properties for each child type.
foreach (var nestedType in type.GetNestedTypes())
{
foreach (var tuple in this.GetTuplesRecursive(prefix + nestedType.Name, nestedType))
{
yield return tuple;
}
}
}
}
And then make a small project that has a reference to all your assemblies with [GenerateResource]
public class Program
{
static void Main(string[] args)
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
string path = Directory.GetCurrentDirectory();
foreach (string dll in Directory.GetFiles(path, "*.dll"))
{
assemblies.Add(Assembly.LoadFile(dll));
}
assemblies = assemblies.Distinct().ToList();
new ResourceGenerator(assemblies);
}
}
Then your attributes can use the static class ResourceFileName.ThisSupports.NestedClasses.ResourceKey while other code can use the resource file.
You might need to tailor it to your specific needs.

Categories