I'm a bit confused about C#'s use of attributes. At first I thought it was simply used to give program code additional information through the use of the [Obsolete] attribute. Now I find that [Dllimport] can be used to import a dynamic linked library and its functions. Can attributes import .exe files and other kind of files?
A last question, for programmers working in C# every day, how much do you use attributes, and do you use it for anything else than extending information and importing dll's?
Simply said, attributes are just metadata attached to classes or methods, at the very base.
The compiler, however, reads through your code, and runs specific actions for specific attributes it encounters while doing so, hardcoded into it. E.g., when it finds a DllImportAttribute on a method, it will resolve it to an external symbol (again, this is a very simplified explanation).
When it finds an ObsoleteAttribute, it emits a warning of deprecation.
Your own attributes (which you can create with a class inheriting from the Attribute base class) will not have an effect on the default compiler. But you (or other libraries) can also scan for them at runtime, opening up many possibilities and leading to your second question:
I typically use them to do meta programming. For example, imagine a custom network server handling packets of a specific format, implemented in different classes. Each packet format is recognized by reading an integer value. Now I need to find the correct class to instantiate for that integer.
I could do that with a switch..case or dictionary mapping integer -> packet which I extend every time I add a packet, but that is ugly since I have to touch code possibly far away from the actual Packet class whenever I add or delete a packet. I may not even know about the switch or dictionary in case the server is implemented in another assembly than my packets (modularity / extensibility)!
Instead, I create a custom PacketAttribute, storing an integer property set via the attribute, and decorate all my Packet classes with it. The server only has to scan through my assembly types at startup (via reflection) and build a dictionary of integer -> packet pairs automatically. Of course I could scan my assembly every time I need a packet, but that's probably a bit slow performance-wise.
There are APIs which are much more attribute heavy, like controllers in ASP.NET Core: You map full request URLs to methods in handler classes with them, which then execute the server code. Even URL parameters are mapped to parameters in that way.
Debuggers can also make use of attributes. For example, decorating a class with the DebuggerDisplayAttribute lets you provide a custom string displayed for the instances of the class when inspecting them in Visual Studio, which has a specific format and can directly show the values of important members.
You can see, attributes can be very powerful if utilized nicely. The comments give some more references! :)
To answer the second part of your questions, they are also used, for example, in setting validation and display attributes for both client and server side use in a web application. For example:
[Display(Name = "Person's age")]
[Required(ErrorMessage = "Persons's age is required")]
[RangeCheck(13, 59, ErrorMessage = "The age must be between 13 and 59")]
public int? PersonsAgeAtBooking { get; set; }
Or to decorate enums for use in display
public enum YesNoOnlyEnum
{
[Description("Yes")]
Yes = 1,
[Description("No")]
No = 2
}
There are many other uses.
We have a system that manages generic physical resources. There are over 500 individual resources. The system is used for many different things and to make the software easier to write we use aliases.
For example, a physical resource TG67I9 is given an alias of "RightDoor". When code is written RightDoor is used instead of TG67I9 making the code more readable. This alias list is loaded as a text file with references to resources and their aliases. This system uses literally hundreds of different alias lists to reference the same physical resources.
This type of setup has two major shortcomings. First, when resources are called using their aliases, they are passed in as strings. Door.Open("RightDoor") for example. This does not give any tooltips or smart anything making the code more difficult to write. It basically requires constantly referencing the alias list. Is it RightDoor or Right_Door or right-door or... you get the idea. The second is that there is no validation of parameters until execution. All the compiler knows is that a string is passed in and then it's happy. Only when the code is run, the function tries to access the resource through its alias and fails because it can't find right-door because it's supposed to be RightDoor. An error is displayed. This requires tedious debugging and running the code over and over to weed out any bad aliases.
Is there a better way to do this? Such that an alias list can be made with a cross-reference of physical resources to their alias names and after the list is made that tooltips could appear suggesting resources. (Assume that a new system could be written from scratch)
I'm using the latest .NET with VisualStudio 2017 and C# to write the code.
The simplest approach is most likely a "string enum":
public class Resources {
public const string
LeftDoor = "TG67I8",
RightDoor = "TG67I9";
}
Sample use:
Door.Open(Resources.RightDoor);
Hovering over .RightDoor in VS shows a tooltip (constant) string Resources.RightDoor = "TG67I9"
Right-clicking .RightDoor and selecting Find All References will show where the variable is used.
Another option can be adding the strings in the Resources section of the Project Properties, and then:
using YourProjectNameSpace.Properties;
...
Door.Open(Resources.RightDoor);
That is a bit slower, because the resource(s) are retrieved at run-time, but allows to load the resources from a custom external file separate from the executable.
Use a static class with constants. I have done the same many times and still do. Plus .NET does this as well.
public static class PhysicalResources
{
public const string One = "Uno";
public const string Two = "Deux";
// ...
}
What I am trying to do:
I am working on a chat bot service where I have different channels of access. On one channel I want to use my Customer.resx file and on another channel I want to use my Worker.resx file.
The problem:
I know how localization works to switch between Worker.en-US.resx and Worker.es-MX.resx, but I am unsure how to switch from one normal resx file to a completely different context one. These will each be translated into other languages so I can't simply use Customer.en-US.resx and Worker.es-MX.resx as a workaround. I do know that I can build a custom culture using the CultureAndRegionInfoBuilder, but I would rather do this in a simpler way than that, something along the lines of how you do localization. I am currently building custom cultures using this method and I have Resources.worker-en-US.resx and similar for customer, but this is a round-about way and I know there must be a better way.
For clarification I know I can grab a resource file with ResXResourceSet, but the idea was that I would like to have two separate resource files with localization for each (worker.en-US and customer.en-US).
I would combine both of these as into one because resx's are really tied to cultures/languages. You can use a strategy pattern to get the right strings at the right time.
interface IStrings { string Foo { get; } }
class WorkerStrings : IStrings { ... }
class CustomerStrings : IStrings { ... }
In an ASP.NET MVC 5 web application, I have a Title string property in my model which is easily bound to a TextBoxFor.
However, because of localization needs, I turned the Title property into a Dictionary<CultureInfo, string>. I've already read around the net that I can bind the TextBoxFor to, say, Model.Title[new CultureInfo("en-US")].
Question #1: am I correct when I assume I can also bind to Model.Title[Model.CurrentLanguage] (or another variable holding the relevant CultureInfo)?
The main problem arises when there is no localization yet, i.e., I'm asking for a given localization for the first time. The dictionary does not contain the key yet, so the binding fails with an exception.
Question #2: how could I manage the missing key case? I know I could pre-fill the dictionary with all needed cultures having an empty or null string, but I would really rather not do that, as it removes flexibility, and it would create many entries in each dictionary while I may only need one or two.
EDIT
Maybe it wasn't clear from the question, but the localizations I'm talking about are user data, and will be read/written from/to a database. They are not application resources.
IMO I would go into resource files rather than dictionary way of localization. Just like here: https://docs.asp.net/en/latest/fundamentals/localization.html
In this way even if you don't have, let's say, fr version of you localized string (no xxx.fr.resx file), it will return default string (which will be the one in main resource file - the one without culture - xxx.resx)
To me, it looks like you're doing the decision-making in the wrong place. Rather than passing a dictionary containing titles for different cultures, you should do your culture/title selection in your controller and pass only what you need to the view. Then you can bind to a String title.
I am currently involved in writing an ASP.NET MVC 4 web version (using the Razor view engine) of an existing (Delphi) desktop based software product which at present allows customers (businesses) to completely customise all of the text in their instance of the application, both to localise it and to customise it to their specific environments.
For example the terms-
My tasks
Products
Workflows
Designs
Might all be changed to individual terms used within the business.
At present this customisation is simply done within the text strings which are stored within the application database, and compared and loaded on every form load in the Delphi database. I.e. every string on the form is compared with the database English strings and a replacement based on the selected locale is rendered on the form if available. I don't feel this is either scalable or especially performant.
I am also not personally comfortable with the idea of customisation happening within the localization method, that every string in the application can be changed by the end customer - it can lead to support issues in terms of consistency in text, and confusion where instructions are incorrectly changed or not kept up to date. There are lots of strings within an application that probably should not be changed beyond localizing them to the locale of the user - local language and/or formatting conventions.
I personally would rather stick with the ASP.NET APIs and conventions in localizing the web version of the application, using RESX resource files and resource keys rather than string matching. This is much more flexible than string matching where strings may have different contexts or cases and cannot simply be changed en-mass (there many English words which may have different meanings in different contexts, and may not map to the same set of meanings in other languages), crucially avoids round trips to the database to fetch the strings needed to fetch the page and also allows for ease of translation with a great set of tools around the standard RESX files. It also means no custom implementation is needed to maintain or document for future developers.
This does however give a problem of how we cope with these custom terms.
I'm currently thinking that we should have a separate RESX file for these terms, which lists defaults for the given locale. I'd then create a new database table which will be something like
CREATE TABLE [dbo].[WEB_CUSTOM_TERMS]
(
[TERM_ID] int identity primary key,
[COMPANY_ID] int NOT NULL, -- Present for legacy reasons
[LOCALE] varchar(8) NOT NULL,
[TERM_KEY] varchar(40) NOT NULL,
[TERM] nvarchar(50) -- Intentionally short, this is to be used for single words or short phrases
);
This can potentially read into a Dictionary<string, string> when needed and cached by IIS to provide lookup without the delay in connecting to the SQL server and conducting the query.
public static class DatabaseTerms
{
private static string DictionaryKey
{
get { return string.Format("CustomTermsDictionary-{0}", UserCulture); }
}
private static string UserCulture
{
get { return System.Threading.Thread.CurrentThread.CurrentCulture.Name; }
}
public static Dictionary<string, string> TermsDictionary
{
get
{
if (HttpContext.Current.Cache[DictionaryKey] != null)
{
var databaseTerms = HttpContext.Current.Cache[DictionaryKey] as Dictionary<string, string>;
if (databaseTerms != null)
{
return databaseTerms;
}
}
var membershipProvider = Membership.Provider as CustomMembershipProvider;
int? companyId = null;
if (membershipProvider != null)
{
companyId = CustomMembershipProvider.CompanyId;
}
using (var context = new VisionEntities())
{
var databaseTerms = (from term in context.CustomTerms
where (companyId == null || term.CompanyId == companyId) &&
(term.Locale == UserCulture)
orderby term.Key
select term).ToDictionary(t => t.Key, t => t.Text);
HttpContext.Current.Cache.Insert(DictionaryKey, databaseTerms, null, DateTime.MaxValue,
new TimeSpan(0, 30, 0), CacheItemPriority.BelowNormal, null);
return databaseTerms;
}
}
set
{
if (HttpContext.Current.Cache[DictionaryKey] != null)
{
HttpContext.Current.Cache.Remove(DictionaryKey);
}
HttpContext.Current.Cache.Insert(DictionaryKey, value, null, DateTime.Now.AddHours(8),
new TimeSpan(0, 30, 0), CacheItemPriority.BelowNormal, null);
}
}
}
I can then have a class which exposes public properties, returning a string based on either this dictionary value or the value in the RESX file - whichever is not null. Something like-
public static class CustomTerm
{
public static string Product
{
get
{
return (DatabaseTerms.TermsDictionary.ContainsKey("Product") ?
DatabaseTerms.TermsDictionary["Product"] : CustomTermsResources.Product);
}
}
}
These can then be added to larger localised strings using string formatting if required, or used by themselves as labels for menus etc.
The main disadvantage of this approach is the need to anticipate in advance which terms the end customers may wish to customise, but I do feel this might present the best of both worlds.
Does this seem like a workable approach and how have other devs approached this problem?
Thanks in advance.
I once designed an MVC application, whereby any string could be changed. In my case it was to handle other languages, but conceivably you could change anything just for aesthetic purposes. That and there is potential for the system to be marketed to other shops, and they may well call the same things different name (You say "Deferred Payment", I say "Lease Payment", etc.)
Warning: This solution is not about globalization and localization (e.g. left-to-right, word/verb ordering - it only needed to do what it did!)
It also considered the possibility of American English (en-US) vs British English (en-GB) vs Australian English (en-AU).
In the end, A Locale table was created in the database:
_id _localeName _idRoot
---------------------------
1 en-GB null
2 en-US 1
3 en-AU 2
Note how US and AU effectively have en-GB as their parent. en-GB therefore had every conceivably string that can be used in the application, in our translation table:
_id _idCulture _from _to
--------------------------------------
1 1 msgyes Yes
2 1 msgno No
3 1 msgcolour Colour
4 2 msgcolour Color
Now, during application initalisation, there was a config flag that specified the culture, which in my case happened to be en-AU. The system looks up the culture tree (en-AU derives from en-GB), and loads all the translations bottom up in to a dictionary cache. Therefore any en-AU specific translations overwrote the GB ones.
So, to describe it in your case - you'd have ALL translations in your database anyway, and that's your default setup. When the customer wishes to customise the text, they basically get a new node (or a derived culture in my example), and you build your cache again. Any terms they customised override the defaults. You no longer have to worry about what terms were done, it just works.
We have a similar setup in our application, we allow certain modules to have a custom names to fit the customers brand.
the first step to this solution is we know our client context at runtime and we stuff it into the HttpContext.Items.
For those items that can be customized, we introduced resource file containing the base keys. If the enterprise wants it customized we add a prefix in front of the key name (ie Client_key)
At once all this is in place its a simple coalesce to fetch the customized or default value.
Resx file snippet
<data name="TotalLeads" xml:space="preserve">
<value>Total Leads</value>
</data>
<data name="Client_TotalLeads" xml:space="preserve">
<value>Total Prospects</value>
</data>
Class to handle switch between custom and base resources
public static class CustomEnterpriseResource
{
public static string GetString(string key)
{
return GetString(key, Thread.CurrentThread.CurrentUICulture);
}
public static string GetString(string key, string languageCode)
{
return GetString(key, new CultureInfo(languageCode));
}
public static string GetString(string key, CultureInfo cultureInfo)
{
var customKey = ((EnterpriseContext)HttpContext.Current.Items[EnterpriseContext.EnterpriseContextKey]).ResourcePrefix + key;
return Resources.Enterprise.ResourceManager.GetString(customKey, cultureInfo)
?? Resources.Enterprise.ResourceManager.GetString(key, cultureInfo);
}
}
Also to assist in the views we create a html helper for this.
public static class EnterpriseResourceHelper
{
/// <summary>
/// Gets a customizable resource
/// </summary>
/// <param name="helper">htmlHelper</param>
/// <param name="key">Key of the resource</param>
/// <returns>Either enterprise customized resource or base resource for current culture.</returns>
public static string EnterpriseResource(this HtmlHelper helper, string key)
{
return CustomEnterpriseResource.GetString(key);
}
}
The requirement you have is not very common. I have worked in projects where localization is done purely using satellite assemblies and in projects where localization is done purely using database tables. In .NET, the recommended approach is RESX files compiled into satellite assemblies. It is a real good approach if you adopt it fully.
Your requirements are some where in between. Though the approach you plan to take, at this point sounds good on paper, I have a feeling over the course of time, maintenance will be difficult, since some of the strings will be in RESX and some will be in database. Even if the distribution is 90% - 10%, people will have difficulty figuring out where it takes the strings when you have all the strings loaded up in production. You will get queries from your users why a particular string is not showing up correctly and at that time it can get difficult for a developer (other than you) to figure out. You will be the best judge for your needs but I believe either you embrace RESX approach fully (which is not possible in your case) or go the full database route. If I have every thing in tables, all I need to do is to run a query for a given profile and I will see all the strings. This will be easier to support.
Even with database, you can follow a RESX-style approach of storing the full string against a key for a culture. The older approach of storing word by word is definitely not a good solution and will not work for different languages, since only sentences can be translated and not individual words. Your idea of caching is definitely needed for performance. So, basically having every thing in a bunch of tables, caching the same in memory and pulling the strings from cache based on the current culture is something I will go for. Of course, my opinion is based on what I could understand by reading your question :).
Also, check this out.
Very interesting question, thanks for bringing it up.
I have localized applications in very different ways, and your case is very specific. Let's start from the fact that everything comes down to localizing the labels/titles of the UI. Therefore, these elements must become localizable. On many platforms (such as WinForms, ASP.NET) they are localizable by design, and all it takes is extending the resource management model. I would say, this is the most natural way of localization if you are writing for such a platform.
In case of ASP.NET MVC, even though it's built on top of ASP.NET engine, we are not recommended to use the ASP.NET's server side tags and therefore the solution does not work. Why I provided it above is to give the clarity to my solution which I'm describing below.
Step 1 - Modularity
All labels and titles are part of some particular screen of the application. Since the screen is what groups them, I often use it for this exact purpose when describing localization resources. BTW, this is why we have one resx file per screen for the applications. So, we are following the consistent standard here.
To express modularity, define classes that correspond to each screen, and have properties defined on it that correspond to each localizable label or title on the screen.
Pseudo example:
class ProductPageResources
{
public string PageTitle { get; set; }
public string ProductNameLabel { get; set; }
}
Step 2 - Localization
When designing your application screens, stick to the modular resource classes defined above. Use localized strings from the modular resource class to display the labels and titles. If there's a need to add a label to the screen, don't forget to add a new property to the modular resource class too. Since it's ASP.NET MVC, we need to pass the resources alongside with the model. Conceptually it would not be necessary, but doing so gives us the flexibility to replace the resource implementation in the future (e.g. from MS SQL to some other source).
Usage example:
#{
ViewBag.Title = string.format(Model.Resources.PageTitle, Model.Product.Name);
}
...
<label>#Model.Resources.ProductNameLabel</label>
Note that the resource class property returns the localized string for the current culture, or the fallback value (described below) if not found. For the default value to appear as a value, I prepare the resource object by iterating the properties and assigning default values to them if they are empty (because the override was not found).
Step 3 - Customization
[Very nice and descriptive term you've got here, so I will use it.]
I personally don't think that the resource management should be data-driven. Main reason is that it's not dynamic enough. Just recall, that we have modular classes, and we start adding properties to it when we need to display something new on the screen. On the other hand, if you add something to the database, it's not appearing on the screen just so.
Therefore, we have a strongly-typed localization here, and it's very natural way of localizing things. The rest comes from this conclusion.
On your customization/resource administration screen you can use reflection to detect all the modular resource classes and display their properties on the screen for customization. To find the resource classes, you can put them under the same namespace, or you could mark them with some attributes to easier find them in the assembly. Either way works.
To make the modular resource class more display-friendly, you can use attributes to assign descriptions that should display instead of their Pascal-Case names on the screen.
So, our modular resource class becomes something like this:
[Description("Product Page")]
class ProductPageResources
{
[Description("Page Title")]
[DefaultValue("Product Details: {0}")
public string PageTitle { get; set; }
[Description("Product Name (label)")]
[DefaultValue("Name:")]
public string ProductNameLabel { get; set; }
}
Basically, on the customization screen we will see default values for Product Page, and each available localized value. For the last part, you can enumerate all the active cultures of the application and extract the value from the property again. Alternatively, you can use some other way depending on the implementation.
This seems to be an extensive reflection, but after all, Visual Studio does something very similar by allowing us to edit the resource files in the special editor. Bottom line is that you have a precisely working framework.