Best way to implement multi-language/globalization in large .NET project - c#

I'll soon be working on a large c# project and would like to build in multi-language support from the start. I've had a play around and can get it working using a separate resource file for each language, then use a resource manager to load up the strings.
Are there any other good approaches that I could look into?

Use a separate project with Resources
I can tell this from out experience, having a current solution with 12 24 projects that includes API, MVC, Project Libraries (Core functionalities), WPF, UWP and Xamarin. It is worth reading this long post as I think it is the best way to do so. With the help of VS tools easily exportable and importable to sent to translation agencies or review by other people.
EDIT 02/2018: Still going strong, converting it to a .NET Standard library makes it possible to even use it across .NET Framework and NET Core. I added an extra section for converting it to JSON so for example angular can use it.
EDIT 2019: Going forward with Xamarin, this still works across all platforms. E.g. Xamarin.Forms advices to use resx files as well. (I did not develop an app in Xamarin.Forms yet, but the documentation, that is way to detailed to just get started, covers it: Xamarin.Forms Documentation). Just like converting it to JSON we can also convert it to a .xml file for Xamarin.Android.
EDIT 2019 (2): While upgrading to UWP from WPF, I encountered that in UWP they prefer to use another filetype .resw, which is is in terms of content identical but the usage is different. I found a different way of doing this which, in my opinion, works better then the default solution.
EDIT 2020: Updated some suggestions for larger (modulair) projects that might require multiple language projects.
So, lets get to it.
Pro's
Strongly typed almost everywhere.
In WPF you don't have to deal with ResourceDirectories.
Supported for ASP.NET, Class Libraries, WPF, Xamarin, .NET Core, .NET Standard as far as I have tested.
No extra third-party libraries needed.
Supports culture fallback: en-US -> en.
Not only back-end, works also in XAML for WPF and Xamarin.Forms, in .cshtml for MVC.
Easily manipulate the language by changing the Thread.CurrentThread.CurrentCulture
Search engines can Crawl in different languages and user can send or save language-specific urls.
Con's
WPF XAML is sometimes buggy, newly added strings don't show up directly. Rebuild is the temp fix (vs2015).
UWP XAML does not show intellisense suggestions and does not show the text while designing.
Tell me.
Setup
Create language project in your solution, give it a name like MyProject.Language. Add a folder to it called Resources, and in that folder, create two Resources files (.resx). One called Resources.resx and another called Resources.en.resx (or .en-GB.resx for specific). In my implementation, I have NL (Dutch) language as the default language, so that goes in my first file, and English goes in my second file.
Setup should look like this:
The properties for Resources.resx must be:
Make sure that the custom tool namespace is set to your project namespace. Reason for this is that in WPF, you cannot reference to Resources inside XAML.
And inside the resource file, set the access modifier to Public:
If you have such a large application (let's say different modules) you can consider creating multiple projects like above. In that case you could prefix your Keys and resource classes with the particular Module. Use the best language editor there is for Visual Studio to combine all files into a single overview.
Using in another project
Reference to your project: Right click on References -> Add Reference -> Prjects\Solutions.
Use namespace in a file: using MyProject.Language;
Use it like so in back-end:
string someText = Resources.orderGeneralError;
If there is something else called Resources, then just put in the entire namespace.
Using in MVC
In MVC you can do however you like to set the language, but I used parameterized url's, which can be setup like so:
RouteConfig.cs
Below the other mappings
routes.MapRoute(
name: "Locolized",
url: "{lang}/{controller}/{action}/{id}",
constraints: new { lang = #"(\w{2})|(\w{2}-\w{2})" }, // en or en-US
defaults: new { controller = "shop", action = "index", id = UrlParameter.Optional }
);
FilterConfig.cs (might need to be added, if so, add FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); to the Application_start() method in Global.asax
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new ErrorHandler.AiHandleErrorAttribute());
//filters.Add(new HandleErrorAttribute());
filters.Add(new LocalizationAttribute("nl-NL"), 0);
}
}
LocalizationAttribute
public class LocalizationAttribute : ActionFilterAttribute
{
private string _DefaultLanguage = "nl-NL";
private string[] allowedLanguages = { "nl", "en" };
public LocalizationAttribute(string defaultLanguage)
{
_DefaultLanguage = defaultLanguage;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string lang = (string) filterContext.RouteData.Values["lang"] ?? _DefaultLanguage;
LanguageHelper.SetLanguage(lang);
}
}
LanguageHelper just sets the Culture info.
//fixed number and date format for now, this can be improved.
public static void SetLanguage(LanguageEnum language)
{
string lang = "";
switch (language)
{
case LanguageEnum.NL:
lang = "nl-NL";
break;
case LanguageEnum.EN:
lang = "en-GB";
break;
case LanguageEnum.DE:
lang = "de-DE";
break;
}
try
{
NumberFormatInfo numberInfo = CultureInfo.CreateSpecificCulture("nl-NL").NumberFormat;
CultureInfo info = new CultureInfo(lang);
info.NumberFormat = numberInfo;
//later, we will if-else the language here
info.DateTimeFormat.DateSeparator = "/";
info.DateTimeFormat.ShortDatePattern = "dd/MM/yyyy";
Thread.CurrentThread.CurrentUICulture = info;
Thread.CurrentThread.CurrentCulture = info;
}
catch (Exception)
{
}
}
Usage in .cshtml
#using MyProject.Language;
<h3>#Resources.w_home_header</h3>
or if you don't want to define usings then just fill in the entire namespace OR you can define the namespace under /Views/web.config:
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
...
<add namespace="MyProject.Language" />
</namespaces>
</pages>
</system.web.webPages.razor>
This mvc implementation source tutorial: Awesome tutorial blog
Using in class libraries for models
Back-end using is the same, but just an example for using in attributes
using MyProject.Language;
namespace MyProject.Core.Models
{
public class RegisterViewModel
{
[Required(ErrorMessageResourceName = "accountEmailRequired", ErrorMessageResourceType = typeof(Resources))]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
}
}
If you have reshaper it will automatically check if the given resource name exists. If you prefer type safety you can use T4 templates to generate an enum
Using in WPF.
Ofcourse add a reference to your MyProject.Language namespace, we know how to use it in back-end.
In XAML, inside the header of a Window or UserControl, add a namespace reference called lang like so:
<UserControl x:Class="Babywatcher.App.Windows.Views.LoginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MyProject.App.Windows.Views"
xmlns:lang="clr-namespace:MyProject.Language;assembly=MyProject.Language" <!--this one-->
mc:Ignorable="d"
d:DesignHeight="210" d:DesignWidth="300">
Then, inside a label:
<Label x:Name="lblHeader" Content="{x:Static lang:Resources.w_home_header}" TextBlock.FontSize="20" HorizontalAlignment="Center"/>
Since it is strongly typed you are sure the resource string exists. You might need to recompile the project sometimes during setup, WPF is sometimes buggy with new namespaces.
One more thing for WPF, set the language inside the App.xaml.cs. You can do your own implementation (choose during installation) or let the system decide.
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
SetLanguageDictionary();
}
private void SetLanguageDictionary()
{
switch (Thread.CurrentThread.CurrentCulture.ToString())
{
case "nl-NL":
MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("nl-NL");
break;
case "en-GB":
MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("en-GB");
break;
default://default english because there can be so many different system language, we rather fallback on english in this case.
MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("en-GB");
break;
}
}
}
Using in UWP
In UWP, Microsoft uses this solution, meaning you will need to create new resource files. Plus you can not re-use the text either because they want you to set the x:Uid of your control in XAML to a key in your resources. And in your resources you have to do Example.Text to fill a TextBlock's text. I didn't like that solution at all because I want to re-use my resource files. Eventually I came up with the following solution. I just found this out today (2019-09-26) so I might come back with something else if it turns out this doesn't work as desired.
Add this to your project:
using Windows.UI.Xaml.Resources;
public class MyXamlResourceLoader : CustomXamlResourceLoader
{
protected override object GetResource(string resourceId, string objectType, string propertyName, string propertyType)
{
return MyProject.Language.Resources.ResourceManager.GetString(resourceId);
}
}
Add this to App.xaml.cs in the constructor:
CustomXamlResourceLoader.Current = new MyXamlResourceLoader();
Where ever you want to in your app, use this to change the language:
ApplicationLanguages.PrimaryLanguageOverride = "nl";
Frame.Navigate(this.GetType());
The last line is needed to refresh the UI. While I am still working on this project I noticed that I needed to do this 2 times. I might end up with a language selection at the first time the user is starting. But since this will be distributed via Windows Store, the language is usually equal to the system language.
Then use in XAML:
<TextBlock Text="{CustomResource ExampleResourceKey}"></TextBlock>
Using it in Angular (convert to JSON)
Now days it is more common to have a framework like Angular in combination with components, so without cshtml. Translations are stored in json files, I am not going to cover how that works, I would just highly recommend ngx-translate instead of the angular multi-translation. So if you want to convert translations to a JSON file, it is pretty easy, I use a T4 template script that converts the Resources file to a json file. I recommend installing T4 editor to read the syntax and use it correctly because you need to do some modifications.
Only 1 thing to note: It is not possible to generate the data, copy it, clean the data and generate it for another language. So you have to copy below code as many times as languages you have and change the entry before '//choose language here'. Currently no time to fix this but probably will update later (if interested).
Path: MyProject.Language/T4/CreateLocalizationEN.tt
<## template debug="false" hostspecific="true" language="C#" #>
<## assembly name="System.Core" #>
<## assembly name="System.Windows.Forms" #>
<## import namespace="System.Linq" #>
<## import namespace="System.Text" #>
<## import namespace="System.Collections.Generic" #>
<## import namespace="System.Resources" #>
<## import namespace="System.Collections" #>
<## import namespace="System.IO" #>
<## import namespace="System.ComponentModel.Design" #>
<## output extension=".json" #>
<#
var fileNameNl = "../Resources/Resources.resx";
var fileNameEn = "../Resources/Resources.en.resx";
var fileNameDe = "../Resources/Resources.de.resx";
var fileNameTr = "../Resources/Resources.tr.resx";
var fileResultName = "../T4/CreateLocalizationEN.json";//choose language here
var fileResultPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileResultName);
//var fileDestinationPath = "../../MyProject.Web/ClientApp/app/i18n/";
var fileNameDestNl = "nl.json";
var fileNameDestEn = "en.json";
var fileNameDestDe = "de.json";
var fileNameDestTr = "tr.json";
var pathBaseDestination = Directory.GetParent(Directory.GetParent(this.Host.ResolvePath("")).ToString()).ToString();
string[] fileNamesResx = new string[] {fileNameEn }; //choose language here
string[] fileNamesDest = new string[] {fileNameDestEn }; //choose language here
for(int x = 0; x < fileNamesResx.Length; x++)
{
var currentFileNameResx = fileNamesResx[x];
var currentFileNameDest = fileNamesDest[x];
var currentPathResx = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", currentFileNameResx);
var currentPathDest =pathBaseDestination + "/MyProject.Web/ClientApp/app/i18n/" + currentFileNameDest;
using(var reader = new ResXResourceReader(currentPathResx))
{
reader.UseResXDataNodes = true;
#>
{
<#
foreach(DictionaryEntry entry in reader)
{
var name = entry.Key;
var node = (ResXDataNode)entry.Value;
var value = node.GetValue((ITypeResolutionService) null);
if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\n", "");
if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\r", "");
#>
"<#=name#>": "<#=value#>",
<#
}
#>
"WEBSHOP_LASTELEMENT": "just ignore this, for testing purpose"
}
<#
}
File.Copy(fileResultPath, currentPathDest, true);
}
#>
If you have a modulair application and you followed my suggestion to create multiple language projects, then you will have to create a T4 file for each of them. Make sure the json files are logically defined, it doesn't have to be en.json, it can also be example-en.json. To combine multiple json files for using with ngx-translate, follow the instructions here
Use in Xamarin.Android
As explained above in the updates, I use the same method as I have done with Angular/JSON. But Android uses XML files, so I wrote a T4 file that generates those XML files.
Path: MyProject.Language/T4/CreateAppLocalizationEN.tt
## template debug="false" hostspecific="true" language="C#" #>
<## assembly name="System.Core" #>
<## assembly name="System.Windows.Forms" #>
<## import namespace="System.Linq" #>
<## import namespace="System.Text" #>
<## import namespace="System.Collections.Generic" #>
<## import namespace="System.Resources" #>
<## import namespace="System.Collections" #>
<## import namespace="System.IO" #>
<## import namespace="System.ComponentModel.Design" #>
<## output extension=".xml" #>
<#
var fileName = "../Resources/Resources.en.resx";
var fileResultName = "../T4/CreateAppLocalizationEN.xml";
var fileResultRexPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileName);
var fileResultPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileResultName);
var fileNameDest = "strings.xml";
var pathBaseDestination = Directory.GetParent(Directory.GetParent(this.Host.ResolvePath("")).ToString()).ToString();
var currentPathDest =pathBaseDestination + "/MyProject.App.AndroidApp/Resources/values-en/" + fileNameDest;
using(var reader = new ResXResourceReader(fileResultRexPath))
{
reader.UseResXDataNodes = true;
#>
<resources>
<#
foreach(DictionaryEntry entry in reader)
{
var name = entry.Key;
//if(!name.ToString().Contains("WEBSHOP_") && !name.ToString().Contains("DASHBOARD_"))//only include keys with these prefixes, or the country ones.
//{
// if(name.ToString().Length != 2)
// {
// continue;
// }
//}
var node = (ResXDataNode)entry.Value;
var value = node.GetValue((ITypeResolutionService) null);
if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\n", "");
if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\r", "");
if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("&", "&");
if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("<<", "");
//if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("'", "\'");
#>
<string name="<#=name#>">"<#=value#>"</string>
<#
}
#>
<string name="WEBSHOP_LASTELEMENT">just ignore this</string>
<#
#>
</resources>
<#
File.Copy(fileResultPath, currentPathDest, true);
}
#>
Android works with values-xx folders, so above is for English for in the values-en folder. But you also have to generate a default which goes into the values folder. Just copy above T4 template and change the folder in the above code.
There you go, you can now use one single resource file for all your projects. This makes it very easy exporting everything to an excl document and let someone translate it and import it again.
Special thanks to this amazing VS extension which works awesome with resx files. Consider donating to him for his awesome work (I have nothing to do with that, I just love the extension).

I've seen projects implemented using a number of different approaches, each have their merits and drawbacks.
One did it in the config file (not my favourite)
One did it using a database - this worked pretty well, but was a pain in the you know what to maintain.
One used resource files the way you're suggesting and I have to say it was my favourite approach.
The most basic one did it using an include file full of strings - ugly.
I'd say the resource method you've chosen makes a lot of sense. It would be interesting to see other people's answers too as I often wonder if there's a better way of doing things like this. I've seen numerous resources that all point to the using resources method, including one right here on SO.

I don't think there is a "best way". It really will depend on the technologies and type of application you are building.
Webapps can store the information in the database as other posters have suggested, but I recommend using seperate resource files. That is resource files seperate from your source. Seperate resource files reduces contention for the same files and as your project grows you may find localization will be done seperatly from business logic. (Programmers and Translators).
Microsoft WinForm and WPF gurus recommend using seperate resource assemblies customized to each locale.
WPF's ability to size UI elements to content lowers the layout work required eg: (japanese words are much shorter than english).
If you are considering WPF: I suggest reading this msdn article
To be truthful I found the WPF localization tools: msbuild, locbaml, (and maybe an excel spreadsheet) tedious to use, but it does work.
Something only slightly related: A common problem I face is integrating legacy systems that send error messages (usually in english), not error codes. This forces either changes to legacy systems, or mapping backend strings to my own error codes and then to localized strings...yech. Error codes are localizations friend

+1 Database
Forms in your app can even re-translate themselves on the fly if corrections are made to the database.
We used a system where all the controls were mapped in an XML file (one per form) to language resource IDs, but all the IDs were in the database.
Basically, instead of having each control hold the ID (implementing an interface, or using the tag property in VB6), we used the fact that in .NET, the control tree was easily discoverable through reflection. A process when the form loaded would build the XML file if it was missing. The XML file would map the controls to their resource IDs, so this simply needed to be filled in and mapped to the database. This meant that there was no need to change the compiled binary if something was not tagged, or if it needed to be split to another ID (some words in English which might be used as both nouns and verbs might need to translate to two different words in the dictionary and not be re-used, but you might not discover this during initial assignment of IDs). But the fact is that the whole translation process becomes completely independent of your binary (every form has to inherit from a base form which knows how to translate itself and all its controls).
The only ones where the app gets more involved is when a phase with insertion points is used.
The database translation software was your basic CRUD maintenance screen with various workflow options to facilitate going through the missing translations, etc.

I´ve been searching and I´ve found this:
If your using WPF or Silverlight your aproach could be use WPF LocalizationExtension for many reasons.
IT´s Open Source
It´s FREE (and will stay free)
is in a real stabel state
In a Windows Application you could do someting like this:
public partial class App : Application
{
public App()
{
}
protected override void OnStartup(StartupEventArgs e)
{
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("de-DE"); ;
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("de-DE"); ;
FrameworkElement.LanguageProperty.OverrideMetadata(
typeof(FrameworkElement),
new FrameworkPropertyMetadata(
XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));
base.OnStartup(e);
}
}
And I think on a Wep Page the aproach could be the same.
Good Luck!

I'd go with the multiple resource files. It shouldn't be that hard to configure.
In fact I recently answered a similar question on setting a global language based resource files in conjunction with form language resource files.
Localization in Visual Studio 2008
I would consider that the best approach at least for WinForm development.

You can use commercial tools like Sisulizer. It will create satellite assembly for each language. Only thing you should pay attention is not to obfuscate form class names (if you use obfuscator).

I highly recommend ResXManager tool which works with ResourceDirectories (resx) and {x:static} markup.
ResXManager works as addin for visual studio or standalone application.

Most opensource projects use GetText for this purpose. I don't know how and if it's ever been used on a .Net project before.

We use a custom provider for multi language support and put all texts in a database table. It works well except we sometimes face caching problems when updating texts in the database without updating the web application.

Standard resource files are easier. However, if you have any language dependent data such as lookup tables then you will have to manage two resource sets.
I haven't done it, but in my next project I would implement a database resource provider. I found how to do it on MSDN:
http://msdn.microsoft.com/en-us/library/aa905797.aspx
I also found this implementation:
DBResource Provider

Related

Embedding build information in local and CI builds like in Gradle

my question does not target a problem. It is more some kind of "Do you know something that...?". All my applications are built and deployed using CI/CD with Azure DevOps. I like to have all build information handy in the create binary and to read them during runtime. Those applications are mainly .NET Core 2 applications written in C#. I am using the default build system MSBuild supplied with the .NET Core SDK. The project should be buildable on Windows AND Linux.
Information I need:
GitCommitHash: string
GitCommitMessage: string
GitBranch: string
CiBuildNumber: string (only when built via CI not locally)
IsCiBuild: bool (Detecting should work by checking for env variables
which are only available in CI builds)
Current approach:
In each project in the solution there is a class BuildConfig à la
public static class BuildConfig
{
public const string BuildNumber = "#{Build.BuildNumber}#"; // Das sind die Namen der Variablen innerhalb der CI
// and the remaining information...
}
Here tokens are used, which get replaced with the corresponding values during the CI build. To achieve this an addon task is used. Sadly this only fills the values for CI builds and not for the local ones. When running locally and requesting the build information it only contains the tokens as they are not replaced during the local build.
It would be cool to either have the BuildConfig.cs generated during the build or have the values of the variables set during the local build (IntelliSense would be very cool and would prevent some "BuildConfig class could not be found" errors). The values could be set by an MSBuild task (?). That would be one (or two) possibilities to solve this. Do you have ideas/experience regarding this? I did not found that much during my internet research. I only stumbled over this question which did not really help me as I have zero experience with MSBuild tasks/customization.
Then I decided to have a look at build systems in general. Namly Fake and Cake. Cake has a Git-Addin, but I did not find anything regarding code generation/manipulation. Do you know some resources on that?
So here's the thing...
Short time ago I had to work with Android apps namly Java and the build system gradle. So I wanted to inject the build information there too during the CI build. After a short time I found a (imo) better and more elegant solution to do this. And this was modifying the build script in the following way (Scripting language used is Groovy which is based on Java):
def getGitHash = { ->
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = stdout
}
return stdout.toString().trim().replace("\"", "\\\"")
}
def getGitBranch = { ->
def fromEnv = System.getenv("BUILD_SOURCEBRANCH")
if (fromEnv) {
return fromEnv.substring("refs/heads/".length()).replace("\"", "\\\"");
} else {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--abbrev-ref', 'HEAD'
standardOutput = stdout
}
return stdout.toString().trim().replace("\"", "\\\"")
}
}
def getIsCI = { ->
return System.getenv("BUILD_BUILDNUMBER") != null;
}
# And the other functions working very similar
android {
# ...
buildConfigField "String", "GitHash", "\"${getGitHash()}\""
buildConfigField "String", "GitBranch", "\"${getGitBranch()}\""
buildConfigField "String", "BuildNumber", "\"${getBuildNumber()}\""
buildConfigField "String", "GitMessage", "\"${getGitCommitMessage()}\""
buildConfigField "boolean", "IsCIBuild", "${getIsCI()}"
# ...
}
The result after the first build is the following java code:
public final class BuildConfig {
// Some other fields generated by default
// Fields from default config.
public static final String BuildNumber = "Local Build";
public static final String GitBranch = "develop";
public static final String GitHash = "6c87e82";
public static final String GitMessage = "Merge branch 'hotfix/login-failed' into 'develop'";
public static final boolean IsCIBuild = false;
}
Getting the required information is done by the build script itself without depending on the CI engine to fulfill this task. This class can be used after the first build its generated and stored in a "hidden" directory which is included in code analysis but exluded from your code in the IDE and also not pushed to the Git. But there is IntelliSense support. In C# project this would be the obj/ folder I guess. It is very easy to access the information as they are a constant and static values (so no reflection or similar required).
So here the summarized question: "Do you know something to achieve this behaviour/mechanism in a .NET environment?"
Happy to discuss some ideas/approaches... :)
It becomes much easier if at runtime you are willing to use reflection to read assembly attribute values. For example:
using System.Reflection;
var assembly = Assembly.GetExecutingAssembly();
var descriptionAttribute = (AssemblyDescriptionAttribute)assembly
.GetCustomAttributes(typeof(AssemblyDescriptionAttribute), false).FirstOrDefault();
var description = descriptionAttribute?.Description;
For most purposes the performance impact of this approach can be satisfactorily addressed by caching the values so they only need to read once.
One way to embed the desired values into assembly attributes is to use the MSBuild WriteCodeFragment task to create a class file that sets assembly attributes to the values of project and/or environment variables. You would need to ensure that you do this in a Target that executes before before compilation occurs (e.g. <Target BeforeTargets="CoreCompile" ...). You would also need to set the property <GenerateAssemblyInfo>false</GenerateAssemblyInfo> to avoid conflicting with the functionality referenced in the next option.
Alternatively, you may be able to leverage the plumbing in the dotnet SDK for including metadata in assemblies. It embeds the values of many of the same project variables documented for the NuGet Pack target. As implied above, this would require the GenerateAssemblyInfo property to be set to true.
Finally, consider whether GitVersion would meet your needs.
Good luck!

I want to define where on hdd will .cs file going to be generated from a t4, programmaticaly

Okay, so everything is said in the title. I couldn't find anywhere on the web how to do this. I need this since we want to generate a .cs file elswhere, and not in the default destination.
I managed to solve a similar task with the following technic.
Install the T4 Toolbox extension to Visual Studio (probably not needed, I'm not sure).
Wrap your template content into a class like this:
<#+
public class MyTemplate : CSharpTemplate
{
public MyTemplate ()
{
}
public override string TransformText()
{
base.TransformText();
#>
// PUT YOUR TEMPLATE RENDERING HERE
<#+
}
}
#>
Then create another template file and call this template explicitly. There you can configure the output parameters.
<#
var mytemplate = new MyTemplate();
mytemplate.Output.Project = #"MyProject.csproj";
mytemplate.Output.File = #"MyFileRelativeToProjectFolder.cs";
mytemplate.Render();
#>
Please refer to this article for further details.

T4 suppress error message "ErrorGeneratingOutput"

I am working with a set of T4 script that generate partial classes for my entities in a EF application.
Because partial classes need to reside in the same assembly, the script resides in the same project as the entity classes. It also needs access to the compiled assembly when executing.
When an error occured, the script will fail with the output "ErrorGeneratingOutput". This will cause the whole project to NOT compile, because the generated file is an .cs file, with (at that point of time) invalid content.
So thats a vicious dependency circle, which can only be broken if I manually remove the error message from the generated file, and then trigger the build.
If there was a way to suppress the error message (or replace it by an empty string), my life would be much easier.
So the question is: can i change the error handling of a t4 script?
In some cases, this problem can be solved easily.
In my case, the problem was about loading the DLL of the project the T4 script resides in. The assembly directive was placed in the top region of the script (line 5). So i changed the output extension to txt.
<## template language="C#" hostspecific="True" debug="True" #>
<## output extension="txt" #>
<##assembly name="invalidAssemblyName"#>
Then I placed the real output into another file using the EntityFrameworkFileManager.
<## include file="EF.Utility.CS.ttinclude"#>
<#
var fileManager = EntityFrameworkTemplateFileManager.Create(this);
fileManager.StartHeader();
fileManager.StartNewFile("Output.cs");
#>
//content
<#
fileManager.Process();
#>
When the error occured, that an assembly cannot be loaded, the ErrorGeneratingOutput message is printed to the default .txt file, where it does not produce a problem for the compilation. If the assembly can be loaded, the output is printed to the Output.cs file.
This way the project can be build after repairing the initial problem, and the developer doesnt have to take care about the ErrorGeneratingOutput problem, too.
I don't know if it is possible, BUT,
you CAN put the T4 scripts in a seperated project and use an MSBuild Task to copy your generated files to your EF entities project.
Your solution should contain
Your EF entities project, let's call it Entities
An entity generator project (you'll put your T4 scripts here), call it EntitiesGenerator for example
You also need to create a project for the custom MSBuild Task which would copy your generated C# files to your "Entities" project
To do so, create a class library project, MyBuildProcess
Reference the following assembly :
Microsoft.Build.Framework (located in C:\Windows\Microsoft.NET\Framework\v4.0.30319)
Now, let's write the custom task
Add a class file to your project, CopyGeneratedEntities.cs, for example
using System;
using Microsoft.Build.Framework;
using System.IO;
namespace MyBuildProcess
{
public class CopyGeneratedEntities : ITask
{
private IBuildEngine _buildEngine;
public IBuildEngine BuildEngine
{
get { return _buildEngine; }
set { _buildEngine = value; }
}
private ITaskHost _hostObject;
public ITaskHost HostObject
{
get { return _hostObject; }
set { _hostObject = value; }
}
public bool Execute()
{
// Copy generated Product entity to EF project
if (File.Exists(#"C:\MySolution\EntitiesGenerator\ProductEntity.cs"))
{
File.Copy(#"C:\MySolution\EntitiesGenerator\ProductEntity.cs",
#"C:\MySolution\Entities\ProductEntity.cs", true);
}
return true;
}
}
}
Build your project
Now edit the .csproj file correponding to your T4 project (EntitiesGenerator) and reference the custom task by adding the following just under the <Project ... > tag :
<UsingTask AssemblyFile="C:\MySolution\Libs\MyBuildProcess.dll"
TaskName="MyBuildProcess.CopyGeneratedEntities" />
And call the task like this (at the end of the csproj file, before the </Project>) :
<Target Name="AfterBuild">`
<CopyGeneratedEntities />
</Target>
Now, when you build the EntitiesGenerator project,
T4 renders your entities and, once the build is over, your custom task is called and your files are copied to your "Entities" project.
You'll only need to manually reference the generated C# files to your Entities project after the first generation,
then they simply be overwriten.
For more information about MSBuild
see.
MSBuild Team Blog - How To: Implementing Custom Tasks
Microsoft.Build Namespaces

T4 templates error: loading the include file ef.utility.cs.ttinclude returned a null or empty string

I have overridden the controller generation T4 templates (ControllerWithContext.tt) as described here.
I would like to take advantage of the code helper utilities found in EF.utility.CS.ttinclude as used in the POCO model generator T4 template. Therefore I copied the following lines from my Model.tt to my ControllerWithContext.tt.
<## include file="EF.Utility.CS.ttinclude"#>
However, when I try to add a controller I am getting the error message
Loading the include file 'EF.utility.CS.ttinclude' returned a null or empty string
According to the MSDN documentation, this error is because the included file is blank, which it clearly isn't because it works with Model.tt
The only difference I can see is that the overridden ControllerWithContext.tt does not have a Custom Tool defined, whereas the Model.tt has it set to TextTemplatingFileGenerator.
My workaround is to copy the functions I need from ef.utility.cs.ttinclude into my ControllerWithContext.tt, which in itself threw up more errors but which were easily solved.
How can I include T4 templates without a custom tool defined?
Following #DustinDavis' advice, and using the invaluable information found on OlegSych's site, here's what I did:
Created a new project called CodeGenerationTools.
Added project references to
System.Data.Entity.Design
EnvDTE
System.Data.Entity
Microsoft.VisualStudio.TextTemplating.10.0
For this last reference I had to install the correct version of the Visual Studio SDK
Copied the EF.Utility.CS.ttinclude file into the project.
Renamed it CodeGenerationTools.cs
Edited the file and convert all <## import namespace="<name>" #> to using <name>;
Deleted the opening and closing <#+ #>
Added the directive using Microsoft.VisualStudio.TextTemplating;
Extended the class:
public class CodeGenerationTools : TextTransformation
Override the TransformText method
public override string TransformText() {
throw new NotImplementedException();
}
Added empty constructor
public CodeGenerationTools() {
_textTransformation = DynamicTextTransformation.Create(this);
_code = new CSharpCodeProvider();
_ef = new MetadataTools(_textTransformation);
FullyQualifySystemTypes = false;
CamelCaseFields = true;
}
Finally, build this project.
The next steps took place in the main project
- Edited the T4 template file.
- Changed template directive to
<## template language="C#" HostSpecific="True" debug="false" inherits="CodeGenerationTools"#>
- Added the directives
<## assembly name="C:\Visual Studio 2010\Projects\CodeGenerationTools\CodeGenerationTools\bin\Debug\CodeGenerationTools.dll" #>
<## import namespace="CodeGenerationTools" #>
All of which now means I can use the helper methods found in EF.Utility.CS.ttinclude in my own T4 templates, and I have the means to add my own helper methods which will be available to all projects.
If you have Visual Studio 2012 or 2013, install this EF tool to resolve the error.
The answer is that the template processor isn't even trying to get the include file (as confirmed using ProcMon). You can reproduce this using any template, not just the EF.Utility.CS.ttinlcude
Not sure why you need the code but you can always build your own base class, just have it inherit from Microsoft.VisualStudio.TextTemplating.TextTransformation and then put in all the code thats is in the EF.Utility file. Then set the inherits directive to point to your new base class and then you can access those methods from your template.

How can I create a strongly typed structure for accessing files in an XNA Content Project?

Preamble:
I'm working with an XNA Content project to hold all of the various textures (and possibly other resources) that I'm using as part of developing a game.
The default method of loading images from the Content project into an XNA texture object involves the use of hard coded string literals to point to the various files.
I would like to automate the projection of the directory/file tree inside the content project into an object hierarchy to avoid using string literals directly, and to gain the benefits of using strongly typed objects.
Example:
Instead of using
Texture2D tex = Content.Load("Textures/defaultTexture32");
I'd much prefer
Texture2D tex = Content.Load(Content.Textures.defaultTexture32);
Question:
Is there an already existing solution to this problem? (I couldn't find anything with Google)
Extra details:
I'm fairly certain this can be done through a T4 template; probably in conjunction with the DTE tools. I've made an initial attempt to do this, but I keep hitting blocks due to my inexperience with both tool sets, but I've worked with T4MVC in the past which does something similar; unfortunately it projects class structure rather than the file system and is not easily adapted.
I do not require the solution to use T4 or DTE, they simply seem as though they're likely to be part of a solution.
Only including files that are part of the VS project (rather than the entire on-disk file system) would be preferable, but not necessary.
The ability to additionally filter by file types, etc. would be an extra special bonus.
For anyone that doesn't immediately see the benefits of doing this; consider what would happen if you renamed or deleted the file. The application would continue to compile fine, but it would crash at runtime. Perhaps not until a very special set of circumstances are met for a certain file to be accessed. If all the file names are projected into an object structure (which is regenerated every time you build the project, or perhaps even every time you modify) then you will get compile-time errors pointing out the missing resources and possibly avoid a lot of future pain.
Thanks for your time.
Here is a T4-template which will read all the files in a "Textures" folder from your project-directory. Then they'll be written into a class as strings, you can just change Directory.GetFiles() if you wish to limit the file-search.
After adding/removing files you can click "Transform All Templates" in Solution Explorer to generate a new class.
Hope this helps!
<## template debug="false" hostspecific="true" language="C#" #>
<## output extension=".cs" #>
<## import namespace="System.IO" #>
<# var files = Directory.GetFiles(Host.ResolvePath("Textures"), "*.*"); #>
namespace Content
{
class Textures
{
<# foreach (string fileName in files) { #>
public const string <#= Path.GetFileNameWithoutExtension(fileName) #> = #"Textures/<#= Path.GetFileNameWithoutExtension(fileName) #>";
<# } #>
}
}
I created a more complicated T4 template that uses DTE to scan through the VS project.
I'm leaving Ronny Karlsson's answer marked as the accepted answer as he helped me get to this point with his simpler solution, but I wanted to make this available to anyone that might find it useful.
Before you use this code yourself, please remember that I'm new to T4 and DTE, so use with caution, and although it seems to work fine for me your mileage may vary.
This template will create nested namespaces for the project and all the folders inside of it... inside those namespaces it will create a class for each file. The classes contain a set of strings for useful things about the file.
Also note the two variables near the top... they define which project to scan and build objects for, and the second, List<string> AcceptableFileExtensions does as you might expect, and indicates which file extensions to consider for creating objects. For example you might want not want to include any .cs or .txt, etc. files. Or you might. Adjust as appropriate. I've only included png at the moment, since that's all I need right now.
<## template debug="false" hostspecific="true" language="C#" #>
<## output extension=".cs" #>
<## assembly name="EnvDTE" #>
<## import namespace="EnvDTE"#>
<## import namespace="System"#>
<## import namespace="System.IO"#>
<## import namespace="System.Collections.Generic"#>
<#
// Global Variables (Config)
var ContentProjectName = "GameContent";
List<string> AcceptableFileExtensions = new List<string>(){".png"};
// Program
IServiceProvider serviceProvider = (IServiceProvider)this.Host;
DTE dte = (DTE) serviceProvider.GetService(typeof(DTE));
Project activeProject = null;
foreach(Project p in dte.Solution.Projects)
{
if(p.Name == ContentProjectName)
{
activeProject = p;
break;
}
}
emitProject(activeProject, AcceptableFileExtensions);
#>
<#+
private void emitProject(Project p, List<string> acceptableFileExtensions)
{
this.Write("namespace GameContent\r\n{\r\n");
foreach(ProjectItem i in p.ProjectItems)
{
emitProjectItem(i, 1, acceptableFileExtensions);
}
this.Write("}\r\n");
}
private void emitProjectItem(ProjectItem p, int indentDepth, List<string> acceptableFileExtensions)
{
if(String.IsNullOrEmpty(Path.GetExtension(p.Name)))
{
emitDirectory(p, indentDepth, acceptableFileExtensions);
}
else if(acceptableFileExtensions.Contains(Path.GetExtension(p.Name)))
{
emitFile(p, indentDepth);
}
}
private void emitDirectory(ProjectItem p, int indentDepth, List<string> acceptableFileExtensions)
{
emitIndent(indentDepth);
this.Write("/// Directory: " + Path.GetFullPath(p.Name) + "\r\n");
emitIndent(indentDepth);
this.Write("namespace " + Path.GetFileNameWithoutExtension(p.Name) + "\r\n");
emitIndent(indentDepth);
this.Write("{" + "\r\n");
foreach(ProjectItem i in p.ProjectItems)
{
emitProjectItem(i, indentDepth + 1, acceptableFileExtensions);
}
emitIndent(indentDepth);
this.Write("}" + "\r\n" + "\r\n");
}
private void emitFile(ProjectItem p, int indentDepth)
{
emitIndent(indentDepth);
this.Write("/// File: " + Path.GetFullPath(p.Name) + "\r\n");
emitIndent(indentDepth);
this.Write("public static class " + Path.GetFileNameWithoutExtension(p.Name) + "\r\n");
emitIndent(indentDepth);
this.Write("{" + "\r\n");
emitIndent(indentDepth + 1);
this.Write("public static readonly string Path = #\"" + Path.GetDirectoryName(Path.GetFullPath(p.Name)) + "\";" + "\r\n");
emitIndent(indentDepth + 1);
this.Write("public static readonly string Extension = #\"" + Path.GetExtension(p.Name) + "\";" + "\r\n");
emitIndent(indentDepth + 1);
this.Write("public static readonly string Name = #\"" + Path.GetFileNameWithoutExtension(p.Name) + "\";" + "\r\n");
emitIndent(indentDepth);
this.Write("}" + "\r\n" + "\r\n");
}
private void emitIndent(int depth)
{
for(int i = 0; i < depth; i++)
{
this.Write("\t");
}
}
#>
I've never seen anything like this done before. The way I would do it would be to create a separate c# console app that scans either the content folder or the content project xml and writes the desired c# code to a file included in your project. You can then run that as a pre-build step (or manually each time you add content). I'm not familiar with T4 or DTE so I can't comment on those options.
Keep in mind that scanning the content project XML has it's drawbacks. You will be able to extract the type of the content item (or at least the content importer/processor assigned), but it may not pick up all content. For example, 3D models automatically include their referenced textures, so these wouldn't be listed in the content project. This might be fine as you are unlikely to want to reference them directly.

Categories