C# attribute text from resource file? - c#

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.

Related

How can I change a custom attribute property on a generic type <T> method input parameter

I have a generic method that takes T, GetResult<T>()
T represents many objects which have custom attribute [JsonProperty(Required = Required.Always)] on most of their properties. Within GetResult<T>(), I need to change T's properties to [JsonProperty(Required = Required.Default)] and I need the next method which is called from within GetResult to get the modified version of T.
Is that possible? If so, what adjustments would I need to my sample program below?
namespace StackOverflow1
{
using System;
using Newtonsoft.Json;
class Program
{
static void Main(string[] args)
{
GetResult<Person>();
Console.Read();
}
private static T GetResult<T>() where T : Base
{
// Entering this method, T is passed with Name property set to [Required = Required.Always]
// I'm changing T's Name property to [Required = Required.Default]
var properties = typeof(T).GetProperties();
foreach (var property in properties)
{
var customAttributes = property.GetCustomAttributes(true);
foreach (var attribute in customAttributes)
{
if (attribute.GetType().Name == nameof(JsonPropertyAttribute))
{
Console.WriteLine($"Original: {((JsonPropertyAttribute)attribute).Required}");
// this is the change! But it looks like this does not actually change T which I need to forward into BuildSingleResult<T>
((JsonPropertyAttribute)attribute).Required = Required.Default;
Console.WriteLine($"After modification: {((JsonPropertyAttribute)attribute).Required}");
}
}
}
// I need T to be the **modified version** which would have [Required = Required.Default]
return BuildSingleResult<T>();
}
private static T BuildSingleResult<T>() where T : Base
{
// **** this is just to write out JsonPropertyAttribute ***
var props = typeof(T).GetProperties();
foreach (var p in props)
{
var customAttributes = p.GetCustomAttributes(true);
foreach (var attr in customAttributes)
{
if (attr.GetType().Name == nameof(JsonPropertyAttribute))
{
// This shows that T still has [Required = Required.Always] but I need it to have [Required = Required.Default]
Console.WriteLine($"(expecting Default): {((JsonPropertyAttribute)attr).Required}");
}
}
}
// *** end of debug ***
return new Person { Name = "X" } as T;
}
}
class Base
{
[JsonProperty(Required = Required.Always)]
public string Name { get; set; }
}
// This is just to demonstrate using a generic type!
class Person : Base { }
}

Command Line Parser verb help not working?

I have defined options as follows:
public class ArgumentsHeader
{
[VerbOption("configure", HelpText = "Sets configuration on server.")]
public ServerConfigurationArguments ServerConfigurationArguments { get; set; }
[HelpVerbOption]
public string GetUsage(string s)
{
return HelpText.AutoBuild(this, s);//always just 'help' or null showing up here.
}
}
public class ServerConfigurationArguments : ArgumentsBase
{
[Option('f', "filename", HelpText = "Path to JSON configuration file", DefaultValue = "config.json", Required = true)]
public string PathToConfig { get; set; }
}
Then parsing them like this:
string invokedVerb = null;
object invokedVerbInstance = null;
var parser = new Parser(x =>
{
x.MutuallyExclusive = true;
});
var options = new ArgumentsHeader();
if (!parser.ParseArguments(args, options,
(verb, subOptions) =>
{
// if parsing succeeds the verb name and correct instance
// will be passed to onVerbCommand delegate (string,object)
invokedVerb = verb;
invokedVerbInstance = subOptions;
}))
{
Exit(ExitStatus.InvalidArguments);
}
But if I try to run my exe with 'help configure' it will just print out entire help, AND in GetUsage(string) method there is only 'help' command showing up in debugger.
Is it a bug or what?
It IS a bug.
I checked with a program similar to yours and had the same (mis)behavior, then switched to the Command Line project itself, had the same but I think I found the problem.
If you are using the "source" version of Command Line Parser embedded in your project, you may fix it as follows (the code below is from class commandLine.Parser):
private bool TryParseHelpVerb(string[] args, object options, Pair<MethodInfo, HelpVerbOptionAttribute> helpInfo, OptionMap optionMap)
{
var helpWriter = _settings.HelpWriter;
if (helpInfo != null && helpWriter != null)
{
if (string.Compare(args[0], helpInfo.Right.LongName, GetStringComparison(_settings)) == 0)
{
// User explicitly requested help
// +++ FIX
// var verb = args.FirstOrDefault(); // This looks wrong as the first element is always the help command itself
var verb = args.Length == 1 ? null : args[1]; // Skip the help command and use next argument as verb
// --- FIX
if (verb != null)
{
var verbOption = optionMap[verb];
if (verbOption != null)
{
if (verbOption.GetValue(options) == null)
{
// We need to create an instance also to render help
verbOption.CreateInstance(options);
}
}
}
DisplayHelpVerbText(options, helpInfo, verb);
return true;
}
}
return false;
}
Unfortunately, if you link directly to the Command Line Parser DLL I don't think there is any workaround for it. In this case only the author can fix it...
If you are using the NuGet package, here is a quick work around. Store the args with the options so you can forward the actual verb to HelpText.AutoBuild. Also you will need to have an instance of your Verb Option for the HelpText.AutoBuild to inspect.
public class ArgumentsHeader
{
public string[] args { get; set; } = new string[0];
[VerbOption("configure", HelpText = "Sets configuration on server.")]
public ServerConfigurationArguments ServerConfigurationArguments { get; set; } = new ServerConfigurationArguments();
[HelpVerbOption]
public string GetUsage(string verb)
{
if (verb?.ToLower() == "help" && args.Length > 1)
{
verb = args[1];
}
return HelpText.AutoBuild(this, verb);
}
}
Then just create the options with the args.
var options = new ArgumentsHeader { args = args };
You can also pass the args omitting the first item of the array (which is the executable name).
For example (VB.NET):
Sub Main()
Dim args = Environment.GetCommandLineArgs().Skip(1)
Dim result = CommandLine.Parser.Default.ParseArguments(Of InstallOptions, UpdateOptions)(args)
...

How to provide localized validation messages for validation attributes

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.

How to get current property name via reflection?

I would like to get property name when I'm in it via reflection. Is it possible?
I have code like this:
public CarType Car
{
get { return (Wheel) this["Wheel"];}
set { this["Wheel"] = value; }
}
And because I need more properties like this I would like to do something like this:
public CarType Car
{
get { return (Wheel) this[GetThisPropertyName()];}
set { this[GetThisPropertyName()] = value; }
}
Since properties are really just methods you can do this and clean up the get_ returned:
class Program
{
static void Main(string[] args)
{
Program p = new Program();
var x = p.Something;
Console.ReadLine();
}
public string Something
{
get
{
return MethodBase.GetCurrentMethod().Name;
}
}
}
If you profile the performance you should find MethodBase.GetCurrentMethod() is miles faster than StackFrame. In .NET 1.1 you will also have issues with StackFrame in release mode (from memory I think I found it was 3x faster).
That said I'm sure the performance issue won't cause too much of a problem- though an interesting discussion on StackFrame slowness can be found here.
I guess another option if you were concerned about performance would be to create a Visual Studio Intellisense Code Snippet that creates the property for you and also creates a string that corresponds to the property name.
Slightly confusing example you presented, unless I just don't get it.
From C# 6.0 you can use the nameof operator.
public CarType MyProperty
{
get { return (CarType)this[nameof(MyProperty)]};
set { this[nameof(MyProperty)] = value]};
}
If you have a method that handles your getter/setter anyway, you can use the C# 4.5 CallerMemberName attribute, in this case you don't even need to repeat the name.
public CarType MyProperty
{
get { return Get<CarType>(); }
set { Set(value); }
}
public T Get<T>([CallerMemberName]string name = null)
{
return (T)this[name];
}
public void Set<T>(T value, [CallerMemberName]string name = null)
{
this[name] = value;
}
I'd like to know more about the context in which you need it since it seems to me that you should already know what property you are working with in the property accessor. If you must, though, you could probably use MethodBase.GetCurrentMethod().Name and remove anything after get_/set_.
Update:
Based on your changes, I would say that you should use inheritance rather than reflection. I don't know what data is in your dictionary, but it seems to me that you really want to have different Car classes, say Sedan, Roadster, Buggy, StationWagon, not keep the type in a local variable. Then you would have implementations of methods that do the proper thing for that type of Car. Instead of finding out what kind of car you have, then doing something, you then simply call the appropriate method and the Car object does the right thing based on what type it is.
public interface ICar
{
void Drive( decimal velocity, Orientation orientation );
void Shift( int gear );
...
}
public abstract class Car : ICar
{
public virtual void Drive( decimal velocity, Orientation orientation )
{
...some default implementation...
}
public abstract void Shift( int gear );
...
}
public class AutomaticTransmission : Car
{
public override void Shift( int gear )
{
...some specific implementation...
}
}
public class ManualTransmission : Car
{
public override void Shift( int gear )
{
...some specific implementation...
}
}
Use MethodBase.GetCurrentMethod() instead!
Reflection is used to do work with types that can't be done at compile time. Getting the name of the property accessor you're in can be decided at compile time so you probably shouldn't use reflection for it.
You get use the accessor method's name from the call stack using System.Diagnostics.StackTrace though.
string GetPropertyName()
{
StackTrace callStackTrace = new StackTrace();
StackFrame propertyFrame = callStackTrace.GetFrame(1); // 1: below GetPropertyName frame
string properyAccessorName = propertyFrame.GetMethod().Name;
return properyAccessorName.Replace("get_","").Replace("set_","");
}
FWIW I implemented a system like this:
[CrmAttribute("firstname")]
public string FirstName
{
get { return GetPropValue<string>(MethodBase.GetCurrentMethod().Name); }
set { SetPropValue(MethodBase.GetCurrentMethod().Name, value); }
}
// this is in a base class, skipped that bit for clairty
public T GetPropValue<T>(string propName)
{
propName = propName.Replace("get_", "").Replace("set_", "");
string attributeName = GetCrmAttributeName(propName);
return GetAttributeValue<T>(attributeName);
}
public void SetPropValue(string propName, object value)
{
propName = propName.Replace("get_", "").Replace("set_", "");
string attributeName = GetCrmAttributeName(propName);
SetAttributeValue(attributeName, value);
}
private static Dictionary<string, string> PropToAttributeMap = new Dictionary<string, string>();
private string GetCrmAttributeName(string propertyName)
{
// keyName for our propertyName to (static) CrmAttributeName cache
string keyName = this.GetType().Name + propertyName;
// have we already done this mapping?
if (!PropToAttributeMap.ContainsKey(keyName))
{
Type t = this.GetType();
PropertyInfo info = t.GetProperty(propertyName);
if (info == null)
{
throw new Exception("Cannot find a propety called " + propertyName);
}
object[] attrs = info.GetCustomAttributes(false);
foreach (object o in attrs)
{
CrmAttributeAttribute attr = o as CrmAttributeAttribute ;
if (attr != null)
{
// found it. Save the mapping for next time.
PropToAttributeMap[keyName] = attr.AttributeName;
return attr.AttributeName;
}
}
throw new Exception("Missing MemberOf attribute for " + info.Name + "." + propertyName + ". Could not auto-access value");
}
// return the existing mapping
string result = PropToAttributeMap[keyName];
return result;
}
There's also a custom attribute class called CrmAttributeAttribute.
I'd strongly recommend against using GetStackFrame() as part of your solution, my original version of the solution was originally the much neater:
return GetPropValue<string>();
But it was 600x slower than the version above.
Solution # 1
var a = nameof(SampleMethod); //a == SampleMethod
var b = nameof(SampleVariable); //b == SampleVariable
var c = nameof(SampleProperty); //c == SampleProperty
Solution # 2
MethodBase.GetCurrentMethod().Name; // Name of method in which you call the code
MethodBase.GetCurrentMethod().Name.Replace("set_", "").Replace("get_", ""); // current Property
Solution # 3
from StackTrace:
public static class Props
{
public static string CurrPropName =>
(new StackTrace()).GetFrame(1).GetMethod().Name.Replace("set_", "").Replace("get_", "");
public static string CurrMethodName =>
(new StackTrace()).GetFrame(1).GetMethod().Name;
}
you just need to call Props.CurrPropName or Props.CurrMethodName
Solution # 4
Solution for .NET 4.5+:
public static class Props
{
public static string GetCallerName([System.Runtime.CompilerServices.CallerMemberName] String propertyName = "")
{
return propertyName;
}
}
usage: Props.GetCallerName();
Yes, it is!
string test = "test string";
Type type = test.GetType();
PropertyInfo[] propInfos = type.GetProperties();
for (int i = 0; i < propInfos.Length; i++)
{
PropertyInfo pi = (PropertyInfo)propInfos.GetValue(i);
string propName = pi.Name;
}
Try using System.Diagnostics.StackTrace to reflect on the call stack. The property should be somewhere in the call stack (probably at the top if you're calling it directly from the property's code).

Localizing enum descriptions attributes

What is the best way to localize enumeration descriptions in .net?
(See Adding descriptions to enumeration constants for enum description example)
Ideally I would like something that uses ResourceManager and resource files so it fits in with how other areas of the app are localized.
This is what I ended up going with, I didn't see the value in adding a custom attribute class to hold a resource key and then looking up into the resource files - why not just use the enums typename + value as a resource key?
using System;
using System.Resources;
using System.Reflection;
public class MyClass
{
enum SomeEnum {Small,Large};
private ResourceManager _resources = new ResourceManager("MyClass.myResources",
System.Reflection.Assembly.GetExecutingAssembly());
public string EnumDescription(Enum enumerator)
{
string rk = String.Format("{0}.{1}",enumerator.GetType(),enumerator);
string localizedDescription = _resources.GetString(rk);
if (localizedDescription == null)
{
// A localized string was not found so you can either just return
// the enums value - most likely readable and a good fallback.
return enumerator.ToString();
// Or you can return the full resourceKey which will be helpful when
// editing the resource files(e.g. MyClass+SomeEnum.Small)
// return resourceKey;
}
else
return localizedDescription;
}
void SomeRoutine()
{
// Looks in resource file for a string matching the key
// "MyClass+SomeEnum.Large"
string s1 = EnumDescription(SomeEnum.Large);
}
}
My solution, using native decription attribute:
public class LocalizedEnumAttribute : DescriptionAttribute
{
private PropertyInfo _nameProperty;
private Type _resourceType;
public LocalizedEnumAttribute(string displayNameKey)
: base(displayNameKey)
{
}
public Type NameResourceType
{
get
{
return _resourceType;
}
set
{
_resourceType = value;
_nameProperty = _resourceType.GetProperty(this.Description, BindingFlags.Static | BindingFlags.Public);
}
}
public override string Description
{
get
{
//check if nameProperty is null and return original display name value
if (_nameProperty == null)
{
return base.Description;
}
return (string)_nameProperty.GetValue(_nameProperty.DeclaringType, null);
}
}
}
public static class EnumExtender
{
public static string GetLocalizedDescription(this Enum #enum)
{
if (#enum == null)
return null;
string description = #enum.ToString();
FieldInfo fieldInfo = #enum.GetType().GetField(description);
DescriptionAttribute[] attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attributes.Any())
return attributes[0].Description;
return description;
}
}
The Enum declaration
public enum MyEnum
{
[LocalizedEnum("ResourceName", NameResourceType = typeof(ResourceType))]
Test = 0
}
Then call MyEnumInstance.GetLocalizedDescription()
there is an easy solution:
use LocalizedDescription attribute to pass a resource key.
[Serializable]
public class LocalizableDescriptionAttribute:DescriptionAttribute
{
public LocalizableDescriptionAttribute(string resourceKey)
:base(Resources.ResourceManager.GetString(resourceKey))
{ }
}
One way I did it once, was to add an extention method in the same namespace as an enum, which returned a string. In my case it was just hardcoded, but would be no problem getting them from a resource file.
public static string Describe(this SomeEnum e)
{
switch(e)
{
SomeEnum.A:
return "Some text from resourcefile";
SomeEnum.B:
return "Some other text from resourcefile";
...:
return ...;
}
}
Maybe not an extremly smooth or fancy solution, but it works =)
Replace #nairik's method with the following to add support for flags enums.
public static string GetLocalizedDescription(this Enum #enum)
{
if ( #enum == null )
return null;
StringBuilder sbRet = new StringBuilder();
string description = #enum.ToString();
var fields = description.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
foreach ( var field in fields )
{
FieldInfo fieldInfo = #enum.GetType().GetField(field);
DescriptionAttribute[] attributes = ( DescriptionAttribute[] )fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
if ( attributes.Any() )
sbRet.AppendFormat("{0}, ", attributes[0].Description);
else
sbRet.AppendFormat("{0}, ", field);
}
if ( sbRet.Length > 2 )
sbRet.Remove(sbRet.Length - 2, 2);
return sbRet.ToString();
}
and replace NameResourceType in the attribute:
public Type NameResourceType
{
get
{
return _resourceType;
}
set
{
_resourceType = value;
_nameProperty = _resourceType.GetProperty(base.Description, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
}
}
See my table example in this question:
Localisation/I18n of database data in LINQ to SQL
The status type table maps to Enumeration values. The real benefit here is that you can have localisation in your reports and across your applications, and specify external IDs for integration with 3rd parties who don't want your internal values etc. It decouples the enum description from it's value.
You can't have multiple System.ComponentModel.DescriptionAttribute applied (so that option is out).
So add a level of indirection, the description holds a resource name, and then use the localisation support in resources. Clearly users of the enum will need to call your helper method to do this.
This looks good: http://www.codeproject.com/Articles/19980/Data-Binding-an-Enum-with-Descriptions

Categories