Migrating to .NET Core – Overridable Localization – Handling Argument Count Mismatch

You can read my original post to understand how I implemented overridable Localization when using Razor Class Libraries (RCL). TLDR; My implementation allows you to use ViewLocalization in RCLs and additionally, allows you to have an override pattern for any level of assembly nesting/referencing similar to the pattern Razor View detection uses.

To my implementation, I’ve made two improvements. First, if you have a resource string that has string substitution like "Hi {0}" and you don’t pass in a parameter when requesting that through a localizer, the page immediately stops rendering and just displays whatever was generated up until that point. The debug log reveals the following.

FormatException: Index (zero based) must be greater than or equal to zero and less than the size of the argument list.

This happens in the LocalizedHtmlString.WriteTo method. Since I couldn’t modify/override this code and I definitely didn’t want the site to just drop dead in its tracks when parameter counts were wrong, I updated my IStringLocalizer<T> and IHtmlLocalizer<T> classes (or at least the helper method). In a ‘literal’ implementation, it would look like the following. This code checks to see if there are sufficient arguments passed in, and if there are not, it appends more string arguments simply indicating that that argument is missing. This way the site still renders and the developer can easily see where arguments were forgotten.

public virtual LocalizedHtmlString this[ string name ] => this[ name, new object[] { } ];

public virtual LocalizedHtmlString this[ string name, params object[] arguments ] 
{
    var ls = _localizer[name, arguments];

    if ( !ls.IsResourceNotFound( ls ) )
    {
        var matches = parameterCount.Matches( ls.Value );

        var expectedParameters = matches.Count > 0
            ? matches.Cast<Match>()
                .Max( m => int.Parse( m.Groups[ "number" ].Value ) + 1 )
            : 0;

        if ( arguments.Length < expectedParameters )
        {
            var protectedArgs =
                arguments.Concat(
                    Enumerable
                        .Range( arguments.Length, expectedParameters - arguments.Length )
                        .Select( p => $"{{MISSING PARAM #{p}}}" )
                ).ToArray();

            return _localizer[name, protectedArgs];
        }
    }

    return ls;
}

The second improvement I made was to clean up the code a ton using generics. Read the original article to see mention of not being DRY. But now I am. I won’t explain everything that changed but instead just drop the updated source code for your review.

public class ResourceLocalizer<TLocalizer, TLocalized>
{
	private string _resourcesRelativePath;
	protected TLocalizer[] localizers;

	public ResourceLocalizer( IOptions<Microsoft.Extensions.Localization.LocalizationOptions> localizationOptions )
	{
		_resourcesRelativePath = localizationOptions.Value.ResourcesPath ?? string.Empty;
	}

	protected string BuildBaseSuffix( Type resourceType )
	{
		// Given a type of RootNameSpace.NS1.NS2.Class,
		// The return value will be N1.N2.Class and should be used to create localizers 
		// for each assembly passed in on `LocalizationOptions`.  
		//
		// The caller will then need to call
		//		factory.Create( Assembly1.N1.N2.Class, Assembly1);
		//		factory.Create( Assembly2.N1.N2.Class, Assembly2);
		//		...
		//
		//
		// factory.Create will correctly put in the 'ResourcePath' based on the
		// `ResourceLocationAttribute` of each assembly.  Otherwise it assumes that all
		// assemblies have the same resource path set inside `LocalizationOptions.ResourcesPath`.

		var typeInfo = resourceType.GetTypeInfo();
		var rootNamespace = GetRootNamespace( typeInfo.Assembly );
		var resourcePrefix = GetResourcePath( typeInfo.Assembly ) + ".";

		var baseSuffix = typeInfo.FullName.Substring( rootNamespace.Length + 1 );

		// When a base path is passed into the Factory.Create, it'll preprend the
		// 'resource path' so we need to trim 'resource path' namespace prefix.  This
		// probably only comes into play for a 'shared resource' and if the file/class is
		// placed inside the Resources folder.  The proper way to do it is probably to
		// create a SharedResources class at root of assembly/namespacing, then put a 
		// SharedResources.resx file at root of Resources folder.  But I wanted to 
		// keep that dummy class tucked away.
		if ( baseSuffix.StartsWith( resourcePrefix ) )
		{
			baseSuffix = baseSuffix.Substring( resourcePrefix.Length );
		}

		return baseSuffix;
	}

	private string GetResourcePath( Assembly assembly )
	{
		var resourceLocationAttribute = assembly.GetCustomAttribute<ResourceLocationAttribute>();

		// If we don't have an attribute assume all assemblies use the same resource location.
		var resourceLocation = resourceLocationAttribute == null
			? _resourcesRelativePath
			: resourceLocationAttribute.ResourceLocation + ".";

		resourceLocation = resourceLocation
			.Replace( Path.DirectorySeparatorChar, '.' )
			.Replace( Path.AltDirectorySeparatorChar, '.' );

		return resourceLocation;
	}

	private string GetRootNamespace( Assembly assembly )
	{
		var rootNamespaceAttribute = assembly.GetCustomAttribute<RootNamespaceAttribute>();

		if ( rootNamespaceAttribute != null )
		{
			return rootNamespaceAttribute.RootNamespace;
		}

		return assembly.GetName().Name;
	}

	protected string BuildBaseName( string path, string applicationName )
	{
		var extension = Path.GetExtension( path );
		var startIndex = path[ 0 ] == '/' || path[ 0 ] == '\\' ? 1 : 0;
		var length = path.Length - startIndex - extension.Length;
		var capacity = length + applicationName.Length + 1;
		var builder = new StringBuilder( path, startIndex, length, capacity );

		builder.Replace( '/', '.' ).Replace( '\\', '.' );

		// Prepend the application name
		builder.Insert( 0, '.' );
		builder.Insert( 0, applicationName );

		return builder.ToString();
	}

	protected IEnumerable<LocalizedString> GetAllLocalizedStrings( bool includeParentCultures, Func<TLocalizer, bool, IEnumerable<LocalizedString>> getAllStrings )
	{
		var keys = new HashSet<string>();

		foreach ( var l in localizers )
		{
			var allStrings = getAllStrings( l, includeParentCultures ).GetEnumerator();
			var more = true;

			while ( more )
			{
				try
				{
					more = allStrings.MoveNext();
				}
				catch
				{
					more = false;
				}

				if ( more && !keys.Contains( allStrings.Current.Name ) )
				{
					keys.Add( allStrings.Current.Name );
					yield return allStrings.Current;
				}
			}
		}
	}

	protected TLocalizedReturn GetLocalizedString<TLocalizedReturn>(
		string key,
		object[] arguments,
		Func<TLocalizedReturn, string> getValue,
		Func<TLocalizedReturn, bool> isResourceFound,
		Func<TLocalizer, string, object[], TLocalizedReturn> getLocalized
	)
	{
		if ( key == null )
		{
			throw new ArgumentNullException( nameof( key ) );
		}

		var currentLocalizer = 0;
		var ls = getLocalized( localizers[ currentLocalizer ], key, arguments );

		while ( !isResourceFound( ls ) && currentLocalizer < localizers.Length - 1 )
		{
			currentLocalizer++;
			ls = getLocalized( localizers[ currentLocalizer ], key, arguments );
		}

		return VerifyLocalizedStringArguments<TLocalizedReturn>(
			ls,
			arguments,
			getValue,
			isResourceFound,
			args => getLocalized( localizers[ currentLocalizer ], key, args )
		);
	}

	private static Regex parameterCount = new Regex( @"(?<!\{)\{(?<number>[0-9]+).*?\}(?!\})", RegexOptions.Compiled );
	private TLocalizedReturn VerifyLocalizedStringArguments<TLocalizedReturn>(
		TLocalizedReturn ls,
		object[] arguments,
		Func<TLocalizedReturn, string> getValue,
		Func<TLocalizedReturn, bool> isResourceFound,
		Func<object[], TLocalizedReturn> getLocalized )
	{
		if ( isResourceFound( ls ) )
		{
			// https://stackoverflow.com/a/948316/166231
			var matches = parameterCount.Matches( getValue( ls ) );

			var expectedParameters = matches.Count > 0
				? matches.Cast<Match>()
					.Max( m => int.Parse( m.Groups[ "number" ].Value ) + 1 )
				: 0;

			if ( arguments.Length < expectedParameters )
			{
				var protectedArgs =
					arguments.Concat(
						Enumerable
							.Range( arguments.Length, expectedParameters - arguments.Length )
							.Select( p => $"{{MISSING PARAM #{p}}}" )
					).ToArray();

				return getLocalized( protectedArgs );
			}
		}

		return ls;
	}

	// Helpers to make Generics work
	protected bool IsResourceFound( LocalizedString s ) => !s.ResourceNotFound;
	protected bool IsResourceFound( LocalizedHtmlString s ) => !s.IsResourceNotFound;
	protected string GetValue( LocalizedString s ) => s.Value;
	protected string GetValue( LocalizedHtmlString s ) => s.Value;
	protected LocalizedHtmlString GetLocalizedHtmlItem( IHtmlLocalizer l, string key, object[] arguments ) => l[ key, arguments ];
	protected LocalizedString GetLocalizedStringItem( IHtmlLocalizer l, string key, object[] arguments ) => l.GetString( key, arguments );
	protected LocalizedString GetLocalizedStringItem( IStringLocalizer l, string key, object[] arguments ) => l[ key, arguments ];
}

public class StringLocalizer<TResourceSource> : ResourceLocalizer<IStringLocalizer, LocalizedString>, IStringLocalizer<TResourceSource>
{
	public StringLocalizer(
		IStringLocalizerFactory factory,
		IOptions<Microsoft.Extensions.Localization.LocalizationOptions> coreLocalizationOptions,
		IOptions<LocalizationOptions> localizationOptions ) : base( coreLocalizationOptions )
	{
		if ( factory == null )
		{
			throw new ArgumentNullException( nameof( factory ) );
		}
		if ( localizationOptions == null )
		{
			throw new ArgumentNullException( nameof( localizationOptions ) );
		}

		var baseNameSuffix = BuildBaseSuffix( typeof( TResourceSource ) );

		localizers =
			localizationOptions.Value.AssemblyNames
				.Select( a => factory.Create( $"{a}.{baseNameSuffix}", a ) )
				.ToArray();
	}

	public virtual LocalizedString this[ string name ] => GetLocalizedString( name, new object[] { }, GetValue, IsResourceFound, GetLocalizedStringItem );
	public virtual LocalizedString this[ string name, params object[] arguments ] => GetLocalizedString( name, arguments, GetValue, IsResourceFound, GetLocalizedStringItem );

	public IEnumerable<LocalizedString> GetAllStrings( bool includeParentCultures ) => GetAllLocalizedStrings( includeParentCultures, ( l, includeParent ) => l.GetAllStrings( includeParent ) );

	public IStringLocalizer WithCulture( CultureInfo culture ) => throw new NotImplementedException();
}

public class HtmlLocalizer<TResourceSource> : ResourceLocalizer<IHtmlLocalizer, LocalizedHtmlString>, IHtmlLocalizer<TResourceSource>
{
	public HtmlLocalizer(
		IHtmlLocalizerFactory factory,
		IOptions<Microsoft.Extensions.Localization.LocalizationOptions> coreLocalizationOptions,
		IOptions<LocalizationOptions> localizationOptions ) : base( coreLocalizationOptions )
	{
		var baseNameSuffix = BuildBaseSuffix( typeof( TResourceSource ) );

		localizers =
			localizationOptions.Value.AssemblyNames
				.Select( a => factory.Create( baseNameSuffix, a ) )
				.ToArray();
	}

	public virtual LocalizedHtmlString this[ string name ] => GetLocalizedString( name, new object[] { }, GetValue, IsResourceFound, GetLocalizedHtmlItem );
	public virtual LocalizedHtmlString this[ string name, params object[] arguments ] => GetLocalizedString( name, arguments, GetValue, IsResourceFound, GetLocalizedHtmlItem );

	public LocalizedString GetString( string name ) => GetLocalizedString( name, new object[] { }, GetValue, IsResourceFound, GetLocalizedStringItem );
	public LocalizedString GetString( string name, params object[] arguments ) => GetLocalizedString( name, arguments, GetValue, IsResourceFound, GetLocalizedStringItem );

	public IHtmlLocalizer WithCulture( CultureInfo culture ) => throw new NotImplementedException();
	public IEnumerable<LocalizedString> GetAllStrings( bool includeParentCultures ) => GetAllLocalizedStrings( includeParentCultures, ( l, includeParent ) => l.GetAllStrings( includeParent ) );
}

public class ViewLocalizer : ResourceLocalizer<IHtmlLocalizer, LocalizedHtmlString>, IViewLocalizer, IViewContextAware
{
	private readonly IHtmlLocalizerFactory localizerFactory;
	private readonly LocalizationOptions localizationOptions;

	public ViewLocalizer(
		IHtmlLocalizerFactory localizerFactory,
		IOptions<Microsoft.Extensions.Localization.LocalizationOptions> coreLocalizationOptions,
		IOptions<LocalizationOptions> localizationOptions
	) : base( coreLocalizationOptions )
	{
		if ( localizerFactory == null )
		{
			throw new ArgumentNullException( nameof( localizerFactory ) );
		}

		if ( localizationOptions == null )
		{
			throw new ArgumentNullException( nameof( localizationOptions ) );
		}

		this.localizationOptions = localizationOptions.Value;
		this.localizerFactory = localizerFactory;
	}

	public void Contextualize( ViewContext viewContext )
	{
		if ( viewContext == null )
		{
			throw new ArgumentNullException( nameof( viewContext ) );
		}

		// Given a view path "/Views/Home/Index.cshtml" we want a baseName like "MyApplication.Views.Home.Index"
		var path = viewContext.ExecutingFilePath;

		if ( string.IsNullOrEmpty( path ) )
		{
			path = viewContext.View.Path;
		}

		Debug.Assert( !string.IsNullOrEmpty( path ), "Couldn't determine a path for the view" );

		localizers =
			localizationOptions.AssemblyNames
				.Select( a => localizerFactory.Create( BuildBaseName( path, a ), a ) )
				.ToArray();
	}

	public virtual LocalizedHtmlString this[ string name ] => GetLocalizedString( name, new object[] { }, GetValue, IsResourceFound, GetLocalizedHtmlItem );
	public virtual LocalizedHtmlString this[ string name, params object[] arguments ] => GetLocalizedString( name, arguments, GetValue, IsResourceFound, GetLocalizedHtmlItem );

	public LocalizedString GetString( string name ) => GetLocalizedString( name, new object[] { }, GetValue, IsResourceFound, GetLocalizedStringItem );
	public LocalizedString GetString( string name, params object[] arguments ) => GetLocalizedString( name, arguments, GetValue, IsResourceFound, GetLocalizedStringItem );

	public IHtmlLocalizer WithCulture( CultureInfo culture ) => throw new NotImplementedException();
	public IEnumerable<LocalizedString> GetAllStrings( bool includeParentCultures ) => GetAllLocalizedStrings( includeParentCultures, ( l, includeParent ) => l.GetAllStrings( includeParent ) );
}

Migrating to .NET Core – Overridable Localization in Razor Class Libraries

I have to admit, the possibility I was most excited about when migrating to .NET Core Razor Pages projects from my 20 year old code base in WebForms was the use of Razor Class Libraries. Packaging up reusable UI components and their required resources into a single project sounded like bliss.

Previously, I had two problems that Razor Class Libraries were going to overcome.

  1. For shared ‘resources’ (css, js, images, etc.) I had a single Git repository that was nested under the main site projects and just included as regular source files. So, while I had (more or less) one code base, I had to make sure I pulled it every time I worked in a different site. I know there is some sort of feature with Git and nested or linked repositories, but I vaguely recall reading some articles about people complaining about them not functioning as advertised, so I steered clear.
  2. The other problem was UI components. Currently, I was creating all my shareable/core ‘controls/UI’ via C# and control creation inside a CreateChildControls method. While it was not horrible (after making some extension methods to make it a little more fluent), it definitely wasn’t as nice as working in HTML/Razor markup.

However, after getting my POC wired up, I came across this line of code in my old WebForms ascx:

<h1><%# GetString( "Default.lnkModeler" ) %></h1>

GetString was a helper method I wrote that wrapped multiple ResourceManager objects into a single instance and pulled the string from the ResourceManager with the highest precedence: Client Site > Infrastructure.UI > Services and so on.  Every level could have a resource file and no matter which level GetString was called, it would find the correct (possibly overridden) occurrence.  This allowed the client site to override any message it wanted to that came from the core (most common) implementation.

Implementing Overridable Localization

While learning ASP.NET Core, I figured I better do Localization ‘right’.  In my WebForms framework, each ‘level’ had only a single ResourceStrings.resx file.  However, in my Razor Page project, I wanted to leverage the LocalizationOptions.ResourcePath property and use the ViewLocalization class to find *.resx files matching my Razor Page names.  Ryan Brandenburg, of ASP.NET team, mentioned the following in a bug report talking about ViewLocalization not working in external assembly: 

We think the correct way to go about this would be to get a IStringLocalizer from some method internal to the library hosting the views, so:
@ExternalLibrary.GetLocalizer(viewPath) instead of @inject Microsoft.AspNetCore.Mvc.Localization.IViewLocalizer Localizer. If it’s very important to you that this come from the IViewLocalizer for some reason you’ll have to write your own implementation.

Well, it was important to me, or at least important that I tried.  Your mileage may vary, but I was able to make ViewLocalizer, StringLocalizer<T>, and HtmlLocalizer<T> all behave the way I mentioned above.

The two problems to overcome were:

  1. Using the correct Assembly when trying to find the resource.  This is the main problem with ViewLocalizer injection used in views from Razor Class Libraries.  No matter which project the ViewLocalizer is injected in, it sets the Assembly to hostingEnvironment.ApplicationName.  When using a Razor Class Library (or any external library), the Assembly needs to be the assembly attempting to use ViewLocalizer.
  2. Overriding localization at any level.  Although this was a custom feature to our framework, it is basically the same pattern used in finding a Razor page from a Razor Class Library.  If the page is found in ‘same location’ in containing site, it uses that, otherwise it uses the page from the Razor Class Library.  To make this work, we simply required all localization points to contain a list of ‘localizers’ that it can try to find the desired string in instead of a single one from the containing site.

Using the Correct Assembly

For all my implementations, I simply introduced a LocalizationOptions that contained a string array of AssemblyNames that are configured during ConfigureServices defining all the assemblies to search.  In the example below, my ‘framework’ hierarchy is Client refs Template refs Infrastructure.

services.Configure<Infrastructure.Localization.LocalizationOptions>(
	options => options.AssemblyNames = new[] {
		hostingEnvironment.ApplicationName, // client
		new AssemblyName( typeof( Startup ).Assembly.FullName ).Name, // template
		new AssemblyName( typeof( Infrastructure.Localization.LocalizationOptions ).Assembly.FullName ).Name  // infrastructure
	}
);

Once the assembly names were in place, making the ViewLocalizer use the correct assembly wasn’t that bad.  I implemented the IViewContextAware.Contextualize method as the following (pretty much verbatim from ASP.NET Core ViewLocalizer implementation except for the assignment of htmlLocalizers which used each assembly name instead of the single hostingEnvironment.ApplicationName) :

public void Contextualize( ViewContext viewContext )
{
	if ( viewContext == null )
	{
		throw new ArgumentNullException( nameof( viewContext ) );
	}

	// Given a view path "/Views/Home/Index.cshtml" we want a baseName like "MyApplication.Views.Home.Index"
	var path = viewContext.ExecutingFilePath;

	if ( string.IsNullOrEmpty( path ) )
	{
		path = viewContext.View.Path;
	}

	Debug.Assert( !string.IsNullOrEmpty( path ), "Couldn't determine a path for the view" );

	htmlLocalizers =
		localizationOptions.AssemblyNames
			.Select( a => localizerFactory.Create( BuildBaseName( path, a ), a ) )
			.ToArray();
}

For StringLocalizer<T> and HtmlLocalizer<T> I had to steal some code from ResourceManagerStringLocalizerFactory. Both of these classes need to build a ‘localizer’ in their constructor based on TResourceSource. When TResourceSource is passed in, I need to get the class name (including any namespacing) but exclude the root namespace of the containing Assembly. This is where ResourceManagerStringLocalizerFactory.GetRootNamespace comes in. Here is my code to get the class ‘name’ excluding the assembly namespace.

protected string BuildBaseSuffix( Type resourceType )
{
	// Given a type of RootNameSpace.NS1.NS2.Class,
	// The return value will be N1.N2.Class and should be used to create localizers 
	// for each assembly passed in on `LocalizationOptions`.  
	//
	// The caller will then need to call
	//		factory.Create( Assembly1.N1.N2.Class, Assembly1);
	//		factory.Create( Assembly2.N1.N2.Class, Assembly2);
	//		...
	//
	//
	// factory.Create will correctly put in the 'ResourcePath' based on the
	// `ResourceLocationAttribute` of each assembly.  Otherwise it assumes that all
	// assemblies have the same resource path set inside `LocalizationOptions.ResourcesPath`.

	var typeInfo = resourceType.GetTypeInfo();
	var rootNamespace = GetRootNamespace( typeInfo.Assembly );
	return typeInfo.FullName.Substring( rootNamespace.Length + 1 );
}

private string GetRootNamespace( Assembly assembly )
{
	var rootNamespaceAttribute = assembly.GetCustomAttribute<RootNamespaceAttribute>();

	if ( rootNamespaceAttribute != null )
	{
		return rootNamespaceAttribute.RootNamespace;
	}

	return assembly.GetName().Name;
}

Then, similar to the ViewLocalizer, inside the constructors of both these generic classes, their localizers are created via appending each assembly name to the base suffix name:

var baseNameSuffix = BuildBaseSuffix( typeof( TResourceSource ) );

localizers =
	assemblyNames
		.Select( a => factory.Create( $"{a}.{baseNameSuffix}", a ) )
		.ToArray();

Applying Localizer Precedence

Now that my localizers were initialized properly, returning the requested string is pretty straightforward. I just loop through the list of localizers until I get a hit, otherwise return the last attempted LocalizedString – where the value will just default to the key/name passed in.

protected LocalizedString GetLocalizedString( IStringLocalizer[] stringLocalizers, string key, object[] arguments )
{
	if ( key == null )
	{
		throw new ArgumentNullException( nameof( key ) );
	}

	var ls = stringLocalizers[ 0 ][ key, arguments ];
	var currentLocalizer = 1;

	while ( ls.ResourceNotFound && currentLocalizer < stringLocalizers.Length )
	{
		ls = stringLocalizers[ currentLocalizer ][ key, arguments ];
		currentLocalizer++;
	}

	return ls;
}

Performance Concerns

I am using as much of the code from the framework as possible, so don’t think I will be causing any additional overhead except for obviously loading a localizer for each assembly name passed in. However, I was a bit surprised that the internal localizer cache inside ResourceManagerStringLocalizerFactory used different methods of generating a cache key based on Create( Type ) vs Create( string, string ) method calls. That said, assuming the overhead of attempting to find a resource, and not finding a match, isn’t too expensive (i.e. when the client assembly doesn’t override the requested string), this should be a straight forward implementation to allow you to use ViewLocalizer in Razor Class Library projects mimicking the same pattern of a client Page/Partial overriding a library Page/Partial.

Dependency Injection Requirements

In addition to the built-in UseLocalization and UseViewLocalization (or UseMvcLocalization which I am not using…yet), you need to use the newly created classes.

services.TryAddTransient( typeof( IStringLocalizer<> ), typeof( Infrastructure.Localization.StringLocalizer<> ) );
services.TryAddTransient( typeof( IHtmlLocalizer<> ), typeof( Infrastructure.Localization.HtmlLocalizer<> ) );
services.TryAddTransient( typeof( IViewLocalizer ), typeof( Infrastructure.Localization.ViewLocalizer ) );

Below you will find my final source code.  I wanted to keep all the ‘weird’ logic in one file, so all my classes derive from a single ResourceLocalizer.  Unfortunately, with no shared interfaces or base classes between StringLocalizer and HtmlLocalizer I couldn’t think of a clever way to keep the code completely DRY.  I have identical code for GetLocalizedString and GetAllStrings except for the parameter type passed in.

Drop a comment if you have any concerns, improvements, or questions…I’m still learning.

public class ResourceLocalizer
{
	protected string BuildBaseSuffix( Type resourceType )
	{
		// Given a type of RootNameSpace.NS1.NS2.Class,
		// The return value will be N1.N2.Class and should be used to create localizers 
		// for each assembly passed in on `LocalizationOptions`.  
		//
		// The caller will then need to call
		//		factory.Create( Assembly1.N1.N2.Class, Assembly1);
		//		factory.Create( Assembly2.N1.N2.Class, Assembly2);
		//		...
		//
		//
		// factory.Create will correctly put in the 'ResourcePath' based on the
		// `ResourceLocationAttribute` of each assembly.  Otherwise it assumes that all
		// assemblies have the same resource path set inside `LocalizationOptions.ResourcesPath`.

		var typeInfo = resourceType.GetTypeInfo();
		var rootNamespace = GetRootNamespace( typeInfo.Assembly );
		return typeInfo.FullName.Substring( rootNamespace.Length + 1 );
	}

	private string GetRootNamespace( Assembly assembly )
	{
		var rootNamespaceAttribute = assembly.GetCustomAttribute<RootNamespaceAttribute>();

		if ( rootNamespaceAttribute != null )
		{
			return rootNamespaceAttribute.RootNamespace;
		}

		return assembly.GetName().Name;
	}

	protected string BuildBaseName( string path, string applicationName )
	{
		var extension = Path.GetExtension( path );
		var startIndex = path[ 0 ] == '/' || path[ 0 ] == '\\' ? 1 : 0;
		var length = path.Length - startIndex - extension.Length;
		var capacity = length + applicationName.Length + 1;
		var builder = new StringBuilder( path, startIndex, length, capacity );

		builder.Replace( '/', '.' ).Replace( '\\', '.' );

		// Prepend the application name
		builder.Insert( 0, '.' );
		builder.Insert( 0, applicationName );

		return builder.ToString();
	}

	protected IEnumerable<LocalizedString> GetAllStrings( IHtmlLocalizer[] htmlLocalizers, bool includeParentCultures )
	{
		var keys = new HashSet<string>();

		foreach ( var l in htmlLocalizers )
		{
			foreach ( var s in l.GetAllStrings( includeParentCultures ) )
			{
				if ( !keys.Contains( s.Name ) )
				{
					yield return s;
				}
			}
		}
	}

	protected LocalizedHtmlString GetLocalizedHtmlString( IHtmlLocalizer[] htmlLocalizers, string key, object[] arguments )
	{
		if ( key == null )
		{
			throw new ArgumentNullException( nameof( key ) );
		}

		var lhs = htmlLocalizers[ 0 ][ key, arguments ];
		var currentLocalizer = 1;

		while ( lhs.IsResourceNotFound && currentLocalizer < htmlLocalizers.Length )
		{
			lhs = htmlLocalizers[ currentLocalizer ][ key, arguments ];
			currentLocalizer++;
		}

		return lhs;
	}

	protected LocalizedString GetLocalizedString( IHtmlLocalizer[] htmlLocalizers, string key, object[] arguments )
	{
		if ( key == null )
		{
			throw new ArgumentNullException( nameof( key ) );
		}

		var ls = htmlLocalizers[ 0 ].GetString( key, arguments );
		var currentLocalizer = 1;

		while ( ls.ResourceNotFound && currentLocalizer < htmlLocalizers.Length )
		{
			ls = htmlLocalizers[ currentLocalizer ].GetString( key, arguments );
			currentLocalizer++;
		}

		return ls;
	}

	protected IEnumerable<LocalizedString> GetAllStrings( IStringLocalizer[] htmlLocalizers, bool includeParentCultures )
	{
		var keys = new HashSet<string>();

		foreach ( var l in htmlLocalizers )
		{
			foreach ( var s in l.GetAllStrings( includeParentCultures ) )
			{
				if ( !keys.Contains( s.Name ) )
				{
					yield return s;
				}
			}
		}
	}

	protected LocalizedString GetLocalizedString( IStringLocalizer[] stringLocalizers, string key, object[] arguments )
	{
		if ( key == null )
		{
			throw new ArgumentNullException( nameof( key ) );
		}

		var ls = stringLocalizers[ 0 ][ key, arguments ];
		var currentLocalizer = 1;

		while ( ls.ResourceNotFound && currentLocalizer < stringLocalizers.Length )
		{
			ls = stringLocalizers[ currentLocalizer ][ key, arguments ];
			currentLocalizer++;
		}

		return ls;
	}
}

public class ViewLocalizer : ResourceLocalizer, IViewLocalizer, IViewContextAware
{
	private readonly IHtmlLocalizerFactory localizerFactory;
	private readonly LocalizationOptions localizationOptions;
	private IHtmlLocalizer[] htmlLocalizers;

	public ViewLocalizer( IHtmlLocalizerFactory localizerFactory, IOptions<LocalizationOptions> localizationOptions )
	{
		if ( localizerFactory == null )
		{
			throw new ArgumentNullException( nameof( localizerFactory ) );
		}

		if ( localizationOptions == null )
		{
			throw new ArgumentNullException( nameof( localizationOptions ) );
		}

		this.localizationOptions = localizationOptions.Value;
		this.localizerFactory = localizerFactory;
	}

	public void Contextualize( ViewContext viewContext )
	{
		if ( viewContext == null )
		{
			throw new ArgumentNullException( nameof( viewContext ) );
		}

		// Given a view path "/Views/Home/Index.cshtml" we want a baseName like "MyApplication.Views.Home.Index"
		var path = viewContext.ExecutingFilePath;

		if ( string.IsNullOrEmpty( path ) )
		{
			path = viewContext.View.Path;
		}

		Debug.Assert( !string.IsNullOrEmpty( path ), "Couldn't determine a path for the view" );

		htmlLocalizers =
			localizationOptions.AssemblyNames
				.Select( a => localizerFactory.Create( BuildBaseName( path, a ), a ) )
				.ToArray();
	}

	public virtual LocalizedHtmlString this[ string key ] => GetLocalizedHtmlString( htmlLocalizers, key, new object[] { } );
	public virtual LocalizedHtmlString this[ string key, params object[] arguments ] => GetLocalizedHtmlString( htmlLocalizers, key, arguments );

	public LocalizedString GetString( string name ) => GetLocalizedString( htmlLocalizers, name, new object[] { } );
	public LocalizedString GetString( string name, params object[] values ) => GetLocalizedString( htmlLocalizers, name, values );

	public IHtmlLocalizer WithCulture( CultureInfo culture ) => throw new NotImplementedException();
	public IEnumerable<LocalizedString> GetAllStrings( bool includeParentCultures ) => GetAllStrings( htmlLocalizers, includeParentCultures );
}

public class StringLocalizer<TResourceSource> : ResourceLocalizer, IStringLocalizer<TResourceSource>
{
	private IStringLocalizer[] localizers;

	public StringLocalizer(
		IStringLocalizerFactory factory,
		IOptions<LocalizationOptions> localizationOptions )
	{
		if ( factory == null )
		{
			throw new ArgumentNullException( nameof( factory ) );
		}
		if ( localizationOptions == null )
		{
			throw new ArgumentNullException( nameof( localizationOptions ) );
		}

		var baseNameSuffix = BuildBaseSuffix( typeof( TResourceSource ) );

		localizers =
			localizationOptions.Value.AssemblyNames
				.Select( a => factory.Create( $"{a}.{baseNameSuffix}", a ) )
				.ToArray();
	}

	public virtual LocalizedString this[ string name ] => GetLocalizedString( localizers, name, new object[] { } );
	public virtual LocalizedString this[ string name, params object[] arguments ] => GetLocalizedString( localizers, name, arguments );

	public IEnumerable<LocalizedString> GetAllStrings( bool includeParentCultures ) => GetAllStrings( localizers, includeParentCultures );

	public IStringLocalizer WithCulture( CultureInfo culture ) => throw new NotImplementedException();
}

public class HtmlLocalizer<TResourceSource> : ResourceLocalizer, IHtmlLocalizer<TResourceSource>
{
	private IHtmlLocalizer[] htmlLocalizers;

	public HtmlLocalizer(
		IHtmlLocalizerFactory factory,
		IOptions<LocalizationOptions> localizationOptions )
	{
		var baseNameSuffix = BuildBaseSuffix( typeof( TResourceSource ) );

		htmlLocalizers =
			localizationOptions.Value.AssemblyNames
				.Select( a => factory.Create( $"{a}.{baseNameSuffix}", a ) )
				.ToArray();
	}

	public virtual LocalizedHtmlString this[ string name ] => GetLocalizedHtmlString( htmlLocalizers, name, new object[] { } );
	public virtual LocalizedHtmlString this[ string name, params object[] arguments ] => GetLocalizedHtmlString( htmlLocalizers, name, arguments );

	public LocalizedString GetString( string name ) => GetLocalizedString( htmlLocalizers, name, new object[] { } );
	public LocalizedString GetString( string name, params object[] values ) => GetLocalizedString( htmlLocalizers, name, values );

	public IHtmlLocalizer WithCulture( CultureInfo culture ) => throw new NotImplementedException();
	public IEnumerable<LocalizedString> GetAllStrings( bool includeParentCultures ) => GetAllStrings( htmlLocalizers, includeParentCultures );
}

Migrating to .NET Core–Bootstrap Active Link TagHelper–ActivePattern Update

In my previous post about my Bootstrap Active Link TagHelper, I described using a `active-prefix` properties to determine if a ‘parent’ menu should be flagged active if one of its child menus were active.  This had a couple flaws.

Shared Pages

If you have a Razor View that serves up content for multiple pages, this can be problematic.  Previously, I was comparing the `asp-page` value with the `ViewContextData.ActionDescriptor.DisplayName` value.  However, if you have multiple menus built using the same page (but different route values) this is a problem.  Consider the following:

<bs-menu-link asp-page="/Schedules/Descriptions" asp-route-group="Junior"  menu-text="Juniors"></bs-menu-link>
<bs-menu-link asp-page="/Schedules/Descriptions" asp-route-group="Adult" menu-text="Adults"></bs-menu-link>
<bs-menu-link asp-page="/Schedules/Descriptions" asp-route-group="Senior" menu-text="Seniors"></bs-menu-link>

As you can see, when *any* of these menus are clicked, the `asp-page` and the `ViewContextData.ActionDescriptor.DisplayName` values are going to be “/Schedules/Descriptions” for every one resulting in all three being flagged as active.

Route Values

Before, I was using something similar to the following to determine if a ‘parent’ page was active.

var currentPage = ViewContextData.ActionDescriptor.DisplayName;
var active = currentPage.StartsWith( ActivePrefix, StringComparison.InvariantCultureIgnoreCase );

As you can guess, route values are not considered on this value.  So I had to use something like this:

var active = ViewContextData.HttpContext.Request.Path.StartsWith( ActivePrefix );

Route Value in Middle of Url

The final problem I discovered, is that I changed my route values and Urls so that a ‘prefix’ no longer worked.  For example:

Route: /Schedules/Program/{id}/{location}

Request.Path: /Schedules/Program/Junior.JD/Outdoor

To highlight the ‘Juniors’ menu link, I could have used an `active-prefix` of ‘/Schedules/Program/Junior’ and it would have matched all ‘program’ Urls.  However, I want the Juniors link to also highlight when Request.Path is /Schedules/Descriptions/Junior.

Regex to the Rescue

To solve my problem, I changed `ActivePrefix` to `ActivePattern` and perform a Regex match on the Request.Path.  Additionally, to solve the ‘Shared Pages’ problem, if there is an ActivePattern property specified, I do not check for the `asp-page` being the same as the `ViewContextData.ActionDescriptor.DisplayName`.

To accomplish the same menu above with the new syntax, I would do something like this:

<bs-menu-link active-pattern="\/Schedules\/.*\/Junior" asp-page="/Schedules/Descriptions" asp-route-group="Junior"  menu-text="Juniors"></bs-menu-link>
<bs-menu-link active-pattern="\/Schedules\/.*\/Adult" asp-page="/Schedules/Descriptions" asp-route-group="Adult" menu-text="Adults"></bs-menu-link>
<bs-menu-link active-pattern="\/Schedules\/.*\/Senior" asp-page="/Schedules/Descriptions" asp-route-group="Senior" menu-text="Seniors"></bs-menu-link>

And finally, the full source code:

	[HtmlTargetElement( "bs-menu-link" )]
	public class BootstrapMenuLinkTagHelper : TagHelper
	{
		private readonly IHtmlGenerator htmlGenerator;

		public BootstrapMenuLinkTagHelper( IHtmlGenerator htmlGenerator )
		{
			this.htmlGenerator = htmlGenerator;
		}

		[HtmlAttributeName( "menu-text" )]
		public string MenuText { get; set; }
		[HtmlAttributeName( "active-pattern" )]
		public string ActivePattern { get; set; }

		[ViewContext]
		[HtmlAttributeNotBound]
		public ViewContext ViewContextData { get; set; }

		#region - Properties shared with AnchorTagHelper -
		private const string ActionAttributeName = "asp-action";
		private const string ControllerAttributeName = "asp-controller";
		private const string AreaAttributeName = "asp-area";
		private const string PageAttributeName = "asp-page";
		private const string PageHandlerAttributeName = "asp-page-handler";
		private const string FragmentAttributeName = "asp-fragment";
		private const string HostAttributeName = "asp-host";
		private const string ProtocolAttributeName = "asp-protocol";
		private const string RouteAttributeName = "asp-route";
		private const string RouteValuesDictionaryName = "asp-all-route-data";
		private const string RouteValuesPrefix = "asp-route-";

		private IDictionary<string, string> _routeValues;

		[HtmlAttributeName( ActionAttributeName )]
		public string Action { get; set; }
		[HtmlAttributeName( ControllerAttributeName )]
		public string Controller { get; set; }
		[HtmlAttributeName( AreaAttributeName )]
		public string Area { get; set; }
		[HtmlAttributeName( PageAttributeName )]
		public string Page { get; set; }
		[HtmlAttributeName( PageHandlerAttributeName )]
		public string PageHandler { get; set; }
		[HtmlAttributeName( ProtocolAttributeName )]
		public string Protocol { get; set; }
		[HtmlAttributeName( HostAttributeName )]
		public string Host { get; set; }
		[HtmlAttributeName( FragmentAttributeName )]
		public string Fragment { get; set; }
		[HtmlAttributeName( RouteAttributeName )]
		public string Route { get; set; }
		[HtmlAttributeName( RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix )]
		public IDictionary<string, string> RouteValues
		{
			get
			{
				if ( _routeValues == null )
				{
					_routeValues = new Dictionary<string, string>( StringComparer.OrdinalIgnoreCase );
				}

				return _routeValues;
			}
			set
			{
				_routeValues = value;
			}
		}
		#endregion

		public async override Task ProcessAsync( TagHelperContext context, TagHelperOutput output )
		{
			output.TagName = "li";

			if ( MenuText != null )
			{
				var currentPage = ViewContextData.ActionDescriptor.DisplayName;

				// TODO: Might need to add logic for Areas, but not using yet, so leaving as is.
				var isPageActive =
					( !string.IsNullOrEmpty( Page ) && string.IsNullOrEmpty( ActivePattern ) && string.Compare( currentPage, Page, true ) == 0 ) ||
					( !string.IsNullOrEmpty( ActivePattern ) && new Regex( ActivePattern, RegexOptions.IgnoreCase ).IsMatch( ViewContextData.HttpContext.Request.Path ) );

				// TODO: Only using Razor Pages right now, so this logic might not be exactly right
				var isControllerActive =
					string.IsNullOrEmpty( Page ) &&
					!string.IsNullOrWhiteSpace( Controller ) &&
					string.Compare( Controller, (string)ViewContextData.RouteData.Values[ "controller" ], true ) == 0 &&
					string.Compare( Action, (string)ViewContextData.RouteData.Values[ "action" ], true ) == 0;

				if ( isPageActive || isControllerActive )
				{
					if ( output.Attributes.ContainsName( "class" ) )
					{
						output.Attributes.SetAttribute( "class", <pre wp-pre-tag-4=""></pre>quot;{output.Attributes[ "class" ].Value} active" );
					}
					else
					{
						output.Attributes.SetAttribute( "class", "active" );
					}
				}

				if ( !string.IsNullOrWhiteSpace( Page ) || !string.IsNullOrWhiteSpace( Controller ) )
				{
					// Declaring and using a builtin AnchorTagHelper following patterns from:
					// https://stackoverflow.com/a/56910392/166231
					var anchorTagHelper = new AnchorTagHelper( htmlGenerator )
					{
						Action = Action,
						Area = Area,
						Controller = Controller,
						Page = Page,
						Fragment = Fragment,
						Host = Host,
						Protocol = Protocol,
						Route = Route,
						PageHandler = PageHandler,
						RouteValues = RouteValues,
						ViewContext = ViewContextData
					};

					var anchorOutput =
						new TagHelperOutput( "a",
							new TagHelperAttributeList(),
							( useCachedResult, encoder ) =>
								Task.Factory.StartNew<TagHelperContent>( () => new DefaultTagHelperContent() )
						);

					anchorOutput.Content.AppendHtml( MenuText );

					var anchorAttributes = new[]
					{
					ActionAttributeName,
					ControllerAttributeName,
					AreaAttributeName,
					PageAttributeName,
					PageHandlerAttributeName,
					FragmentAttributeName,
					HostAttributeName,
					ProtocolAttributeName,
					RouteAttributeName,
					RouteValuesDictionaryName
				};

					// TODO: Not sure if I have to pass in all these attributes since they are assigned on the anchorTagHelper
					//		 I asked a question to the Stack Overflow post and am awaiting an answer.
					var anchorAttributeList =
						context.AllAttributes
							.Where( a => anchorAttributes.Contains( a.Name ) || a.Name.StartsWith( RouteValuesPrefix, StringComparison.InvariantCultureIgnoreCase ) )
							.Select( a => new TagHelperAttribute( a.Name, a.Value ) );

					var anchorContext = new TagHelperContext(
						new TagHelperAttributeList( anchorAttributeList ),
						new Dictionary<object, object>(),
						Guid.NewGuid().ToString() );

					await anchorTagHelper.ProcessAsync( anchorContext, anchorOutput );

					output.PreContent.SetHtmlContent( anchorOutput );
				}
				else
				{
					output.PreContent.SetHtmlContent( <pre wp-pre-tag-4=""></pre>quot;<div>{MenuText}</div>" );
				}
			}
		}
	}

Migrating to .NET Core–Bootstrap Active Link TagHelper

Update: After reading this post, see the update discussing ActivePattern vs ActivePrefix.

Well, it is finally happening, I’m working on migrating our Web Forms sites to ASP.NET Core.  It’s only 10 years in the making but better late than never.

Given that I’m migrating a framework/product more so than just a ‘site’, I’m sure I’m going to run into a lot of speed bumps while trying to replicate all the features (aka quirky things) we’ve invented over the years.  I figured now is as good as time as ever to also beef up my blog output production.  So every time I figure something out that was a little strange (or just difficult), I’m going to put it down in a blog.

Applying an active class to a listem inside Bootstrap navigation

One of the first problems I had to overcome was getting the ‘active’ class assigned on my Bootstrap 3 navbar menus if it was the active page.  All the solutions I saw had a major flaw (assuming I was reading their code right).  More or less, every sample had to specify the Page or Controller/Action twice with syntax similar to this:

<ul>
    <li class="@Html.IsSelected(actions: "Home", controllers: "Default")">
        <a href="@Url.Action("Home", "Default")">Home</a>
    </li>
    <li class="@Html.IsSelected(actions: "List,Detail", controllers: "Default")">
        <a href="@Url.Action("List", "Default")">List</a>
    </li>
</ul>

Since the active class has to go on the <li/> item, samples all had HtmlHelper extensions or TagHelpers against the list item, but then used Url.Action or AnchorTagHelper in the markup inside that list item, repeating the current Page or Controller/Action for the desired navigation.

Creating a BootstrapMenuLinkTagHelper to solve all the problems

I decided to make a TagHelper that rendered both a li item and child a item together, allowing me to specify the destination one time.

Considerations/Requirements:

  1. Render li/a in one TagHelper
  2. The target element of the TagHelper can contain HTML and should not be lost
  3. Leverage AnchorTagHelper to render the anchor (thus supporting all the asp-* attributes supported by AnchorTagHelper)
  4. Need a mechanism to flag menu links that are parents for sub-navigation resulting in both the parent and the active child link to have the active class applied

In the final code, you’ll see comments/links to Stack Overflow questions or other relevant pages when I followed some code ideas/patterns, so I will not attribute them here.

Render li/a in one TagHelper

I decided to have my own tag name bs-menu-link that would then render as an li item.  Rendering the active class was easy enough. I simply looked at the ViewContext.ActionDescriptor.DisplayName and compared it to the ‘page’ value passed to the TagHelper.  For MVC support, I used ViewContext.RouteData.Values[ "controller" ] and ViewContext.RouteData.Values[ "action" ] values and compared them to the controller/action values passed in (albeit, I didn’t test this because I’m not using MVC).  This appeared to be the correct way to do it, but correct me if I’m wrong.

However, I discovered quickly, that just having my TagHelper render html like <a asp-page="/Index">Home</a> was not going to trigger the AnchorTagHelper Invoke method.  The trick to get it to work is to declare/use a AnchorTagHelper passing in TagHelperOutput and TagHelperContext on the AnchorTagHelper.ProcessAsync method.

var anchorTagHelper = new AnchorTagHelper( htmlGenerator )
{
    ...
};
  
var anchorOutput =
    new TagHelperOutput( "a",
        new TagHelperAttributeList(),
        ( useCachedResult, encoder ) =>
            Task.Factory.StartNew<TagHelperContent>( () => new DefaultTagHelperContent() )
    );
  
var anchorContext = new TagHelperContext(
    new TagHelperAttributeList( new TagHelperAttribute( "someKey", "someValue" ) ),
    new Dictionary<object, object>(),
    Guid.NewGuid().ToString() );
  
await anchorTagHelper.ProcessAsync( anchorContext, anchorOutput );

The target element of the TagHelper can contain HTML and should not be lost

This was rather easy.  Instead of using output.Content.SetHtmlContent( … ) I instead used output.PreContent.SetHtmlContent( … ).

Leverage AnchorTagHelper to render the anchor

As I said, I’m *just* starting ASP.NET Core so I could be doing this wrong, but the best way I saw to do this was include all the same properties that the AnchorTagHelper exposes.  My TagHelper also exposes the following in the same manner as the AnchorTagHelper source:

private const string ActionAttributeName = "asp-action";
private const string ControllerAttributeName = "asp-controller";
private const string AreaAttributeName = "asp-area";
private const string PageAttributeName = "asp-page";
private const string PageHandlerAttributeName = "asp-page-handler";
private const string FragmentAttributeName = "asp-fragment";
private const string HostAttributeName = "asp-host";
private const string ProtocolAttributeName = "asp-protocol";
private const string RouteAttributeName = "asp-route";
private const string RouteValuesDictionaryName = "asp-all-route-data";
private const string RouteValuesPrefix = "asp-route-";
  
private IDictionary<string, string> _routeValues;
  
[HtmlAttributeName( ActionAttributeName )]
public string Action { get; set; }
[HtmlAttributeName( ControllerAttributeName )]
public string Controller { get; set; }
[HtmlAttributeName( AreaAttributeName )]
public string Area { get; set; }
[HtmlAttributeName( PageAttributeName )]
public string Page { get; set; }
[HtmlAttributeName( PageHandlerAttributeName )]
public string PageHandler { get; set; }
[HtmlAttributeName( ProtocolAttributeName )]
public string Protocol { get; set; }
[HtmlAttributeName( HostAttributeName )]
public string Host { get; set; }
[HtmlAttributeName( FragmentAttributeName )]
public string Fragment { get; set; }
[HtmlAttributeName( RouteAttributeName )]
public string Route { get; set; }
[HtmlAttributeName( RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix )]
public IDictionary<string, string> RouteValues
{
    get
    {
        if ( _routeValues == null )
        {
            _routeValues = new Dictionary<string, string>( StringComparer.OrdinalIgnoreCase );
        }
  
        return _routeValues;
    }
    set
    {
        _routeValues = value;
    }
}

I then pass these through when I create the AnchorTagHelper and also when I create/pass the TagHelperAttributeList to the TagHelperContext constructor.  I have tried to determine if I need to pass them both on the AnchorTagHelper constructor and the attribute list and will update the code if it turns out I don’t.

Need a mechanism to flag menu links that are parents for sub-navigation resulting in both the parent and the active child link to have the active class applied

This may be a specific need for our sites/framework, but when one of our nav items has a dropdown menu, we have markup that looks like this:

<li class="dropdown active">
    <a href="/Facilities/Indoor">Membership / Facilities</a>
    <ul class="dropdown-menu">        
        <li><a href="/Facilities/Indoor">Indoor Site</a></li>
        <li class="active"><a href="/Facilities/Outdoor">Outdoor Site</a></li>
        <li><a href="/Facilities/ProShop">The Pro Shop</a></li>
    </ul>
</li>

And the rendered look appears as:

BootstrapDropDownNav

As you can see, the parent link, and the child link are both ‘active’ (green).  Since the parent link (/Facilities/Indoor) will never match the ‘current url’, the only way I could figure out a way to get this flagged as active was to add a property ActivePrefix (using the active-prefix attribute).  To accomplish the above functionality, the Membership/Facilities link would be created as: <bs-menu-link class="dropdown" active-prefix="/Facilities/" asp-page="/Facilities/Indoor" menu-text="Memberships / Facilities">

Note the use of the menu-text attribute.  I needed a way to pass this text in as well to be rendered on the child anchor.  This is accomplished by appending HTML to the TagHelperOutput via anchorOutput.Content.AppendHtml( MenuText );

Usage of the BootstrapMenuLinkTagHelper

To render the markup of the image shown above, it would look like the following (of course you need to register the TagHelper in your _ViewImports).

<bs-menu-link asp-page="/Index" menu-text="Home"></bs-menu-link>
<bs-menu-link class="dropdown" active-prefix="/Facilities/" asp-page="/Facilities/Indoor" menu-text="Memberships / Facilities">
    <ul class="dropdown-menu">
        <bs-menu-link asp-page="/Facilities/Indoor" menu-text="Indoor Site"></bs-menu-link>
        <bs-menu-link asp-page="/Facilities/Outdoor" menu-text="Outdoor Site"></bs-menu-link>
        <bs-menu-link asp-page="/Facilities/ProShop" menu-text="The Pro Shop"></bs-menu-link>
    </ul>
</bs-menu-link>
<bs-menu-link asp-page="/Schedules/Index" menu-text="Schedules"></bs-menu-link>
<bs-menu-link asp-page="/Pros/Index" menu-text="Meet the Pros"></bs-menu-link>
<bs-menu-link asp-page="/Events/Index" menu-text="Events"></bs-menu-link>

Note the use of existing HTML underneath the Facilities bs-menu-link.  This HTML will be retained and the /Facilities/Indoor anchor will be inserted before the contained <ul/>.

Happy coding.  I’m really enjoying coding and learning in ASP.NET Core and I hope my experiences will help some of you.  And finally, the complete source:

[HtmlTargetElement( "bs-menu-link" )]
public class BootstrapMenuLinkTagHelper : TagHelper
{
    private readonly IHtmlGenerator htmlGenerator;
  
    public BootstrapMenuLinkTagHelper( IHtmlGenerator htmlGenerator )
    {
        this.htmlGenerator = htmlGenerator;
    }
  
    [HtmlAttributeName( "menu-text" )]
    public string MenuText { get; set; }
    [HtmlAttributeName( "active-prefix" )]
    public string ActivePrefix { get; set; }
  
    [ViewContext]
    [HtmlAttributeNotBound]
    public ViewContext ViewContextData { get; set; }
  
    #region - Properties shared with AnchorTagHelper -
    private const string ActionAttributeName = "asp-action";
    private const string ControllerAttributeName = "asp-controller";
    private const string AreaAttributeName = "asp-area";
    private const string PageAttributeName = "asp-page";
    private const string PageHandlerAttributeName = "asp-page-handler";
    private const string FragmentAttributeName = "asp-fragment";
    private const string HostAttributeName = "asp-host";
    private const string ProtocolAttributeName = "asp-protocol";
    private const string RouteAttributeName = "asp-route";
    private const string RouteValuesDictionaryName = "asp-all-route-data";
    private const string RouteValuesPrefix = "asp-route-";
  
    private IDictionary<string, string> _routeValues;
  
    [HtmlAttributeName( ActionAttributeName )]
    public string Action { get; set; }
    [HtmlAttributeName( ControllerAttributeName )]
    public string Controller { get; set; }
    [HtmlAttributeName( AreaAttributeName )]
    public string Area { get; set; }
    [HtmlAttributeName( PageAttributeName )]
    public string Page { get; set; }
    [HtmlAttributeName( PageHandlerAttributeName )]
    public string PageHandler { get; set; }
    [HtmlAttributeName( ProtocolAttributeName )]
    public string Protocol { get; set; }
    [HtmlAttributeName( HostAttributeName )]
    public string Host { get; set; }
    [HtmlAttributeName( FragmentAttributeName )]
    public string Fragment { get; set; }
    [HtmlAttributeName( RouteAttributeName )]
    public string Route { get; set; }
    [HtmlAttributeName( RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix )]
    public IDictionary<string, string> RouteValues
    {
        get
        {
            if ( _routeValues == null )
            {
                _routeValues = new Dictionary<string, string>( StringComparer.OrdinalIgnoreCase );
            }
  
            return _routeValues;
        }
        set
        {
            _routeValues = value;
        }
    }
    #endregion
  
    public async override Task ProcessAsync( TagHelperContext context, TagHelperOutput output )
    {
        output.TagName = "li";
  
        if ( MenuText != null )
        {
            var currentPage = ViewContextData.ActionDescriptor.DisplayName;
  
            // TODO: Might need to add logic for Areas, but not using yet, so leaving as is.
            var isPageActive =
                !string.IsNullOrEmpty( Page ) && (
                    string.Compare( currentPage, Page, true ) == 0 ||
                    ( !string.IsNullOrEmpty( ActivePrefix ) && currentPage.StartsWith( ActivePrefix, StringComparison.InvariantCultureIgnoreCase ) )
                );
  
            // TODO: Only using Razor Pages right now, so this logic might not be exactly right
            var isControllerActive =
                string.IsNullOrEmpty( Page ) &&
                string.Compare( Controller, (string)ViewContextData.RouteData.Values[ "controller" ], true ) == 0 &&
                string.Compare( Action, (string)ViewContextData.RouteData.Values[ "action" ], true ) == 0;
  
            if ( isPageActive || isControllerActive )
            {
                if ( output.Attributes.ContainsName( "class" ) )
                {
                    output.Attributes.SetAttribute( "class", @"{output.Attributes[ "class" ].Value} active" );
                }
                else
                {
                    output.Attributes.SetAttribute( "class", "active" );
                }
            }
  
            // Declaring and using a builtin AnchorTagHelper following patterns from:
            // https://stackoverflow.com/a/56910392/166231
            var anchorTagHelper = new AnchorTagHelper( htmlGenerator )
            {
                Action = Action,
                Area = Area,
                Controller = Controller,
                Page = Page,
                Fragment = Fragment,
                Host = Host,
                Protocol = Protocol,
                Route = Route,
                PageHandler = PageHandler,
                RouteValues = RouteValues,
                ViewContext = ViewContextData
            };
  
            var anchorOutput =
                new TagHelperOutput( "a",
                    new TagHelperAttributeList(),
                    ( useCachedResult, encoder ) =>
                        Task.Factory.StartNew<TagHelperContent>( () => new DefaultTagHelperContent() )
                );
  
            anchorOutput.Content.AppendHtml( MenuText );
  
            var anchorAttributes = new[]
            {
                ActionAttributeName,
                ControllerAttributeName,
                AreaAttributeName,
                PageAttributeName,
                PageHandlerAttributeName,
                FragmentAttributeName,
                HostAttributeName,
                ProtocolAttributeName,
                RouteAttributeName,
                RouteValuesDictionaryName
            };
  
            // TODO: Not sure if I have to pass in all these attributes since they are assigned on the anchorTagHelper
            //       I asked a question to the Stack Overflow post and am awaiting an answer.
            var anchorAttributeList =
                context.AllAttributes
                    .Where( a => anchorAttributes.Contains( a.Name ) || a.Name.StartsWith( RouteValuesPrefix, StringComparison.InvariantCultureIgnoreCase ) )
                    .Select( a => new TagHelperAttribute( a.Name, a.Value ) );
  
            var anchorContext = new TagHelperContext(
                new TagHelperAttributeList( anchorAttributeList ),
                new Dictionary<object, object>(),
                Guid.NewGuid().ToString() );
  
            await anchorTagHelper.ProcessAsync( anchorContext, anchorOutput );
  
            output.PreContent.SetHtmlContent( anchorOutput );
        }
    }
}

MSBuild/Visual Studio Does Not Copy Nuget Package Assemblies

tl;dr – Jump to the code to see how I used a custom MSBuild Task that scans all non-GAC’d references (binary and project) of a client/host application to copy all missing dependent assemblies from any level in a solution structure.

The Problem…

Actually, the title doesn’t sum up everything but I needed a catchy search title.  It is mostly true, but in reality, there are cases where standard assembly references to third party assemblies, or even projects I’ve created, aren’t copied either.

Here is my scenario.  I have a hundred plus client projects written against my company’s ‘framework assemblies’ that look like the following:

  • Client Web Application –> references –>
    • Framework Assembly 1
    • Framework Assembly 2 –> references –>
      • Nuget: Newtonsoft.Json
      • Framework Assembly 3 –> references –>
        • Framework Assembly 4 –> references –>
          • Nuget: Newtonsoft.Json
    • Framework Assembly 4 –> references –>
      • Nuget: Newtonsoft.Json    

As you can see, Framework Assembly 4 is a ‘utility’ assembly that can/is referenced at multiple levels in the project architecture.  Also, Newtonsoft.Json is referenced at multiple levels.  The problem is, when I build Client Web Application both Framework Assembly 3 and Newtonsoft.Json are not in the output directory even though all references in the project have CopyLocal=true.

Below is an image of a simple console application reproducing the same problem:

DependentCopySln

When you build ClientApplication, the output directory is as follows:

ReleaseBefore

Notice ClassLibrary3* and Newtonsoft* files are missing.  You can get the source for that sample solution here if you want to replicate the problem yourself.

My first bad solution…

My first solution to this problem was pretty poor.  I simply added the Newtonsoft.Json nuget package to the Client Web Application as well.  This worked for a bit, but there were three main problems…

  1. If the framework assemblies updated their Nuget package and the client application did not, I get a runtime errors due to version conflicts.
  2. If a developer who is not aware of this hack runs an extension like refassistant, they might remove the ‘unused Nuget reference’ from the client application, again experiencing runtime errors for missing assemblies.
  3. If a framework assembly adds another Nuget package assembly, I’d have to update all the client applications to add the new Nuget package as well – painful.

Correct solution, a custom MSBuild Task…

I finally decided to bite the bullet and implement the correct solution, which is to have a custom MSBuild task in the client application project that scans for and copies all required dependent assemblies.

A good walk through of creating a custom MSBuild task can be found at The Custom MSBuild Task Cookbook.  The only tip I would offer is that you need to compile the MSBuild Task project on the x64 Platform Target.  I can’t remember where I saw a mention of this, probably somewhere on Stack Overflow, but many of the walk through examples you’ll find on the net describing the creation of a MSBuild task don’t mention this and you get a confusing error when it tries to execute your task:

The "{task name}" task could not be loaded from the assembly {task assembly file}. Could not load file or assembly ‘{assembly name}’ or one of its dependencies. The system cannot find the file specified. Confirm that the declaration is correct, that the assembly and all its dependencies are available, and that the task contains a public class that implements Microsoft.Build.Framework.ITask.

However, when you review everything mentioned, everything seems in order.  But, simply flipping the Platform Target to x64 makes things start to work.

Building DependencyCopy task…

I came across Recursively Copying Indirect Project Dependencies in MSBuild which was almost what I was after.  My goal is almost identical, but the main problem I had with the solution found there is that all the code was in an inline code task and I would have to insert the 100 or so lines of code into the 100 plus client *.csproj files.  That wasn’t going to be acceptable.  I knew I needed this code in one central, debug-able location and as the author admitted,

Yeah, that’s a lot of code. And I could probably tighten it up, but I’m only using it once, in one place

So, I addition to tightening it up, I had to add one more feature.  My client projects (and my framework assemblies for that matter) do not always have project references.  Actually, I normally reference the assemblies so I don’t have to rebuild extra projects every time I publish a client.  So my code had to find binary references to assemblies built by my company and find dependent references of that project as well.  Because of that, I have some code that is specific to my company that I will present here first:

var btrBinaryReferences =
	project.Elements( ns + "ItemGroup" )
			.Elements( ns + "Reference" )
			.Where( r => ( (string)r.Attribute( "Include" ) ).StartsWith( "BTR." ) )
			.Select( r =>
				GetProjectFileFromBinaryAssembly(
					Path.Combine( projectFolder, (string)r.Element( ns + "HintPath" ) )
				)
			);

As I mentioned, any binary references to my company’s assemblies (files with names starting with BTR.) need to have the associated *.csproj scanned as well.  Once I identify these assemblies with the StartsWith() clause, I then get the *.csproj via GetProjectFileFromBinaryAssembly.  The code in that method has some documented assumptions applied based on BTR’s local file system structures.

Finally, before listing the code, I’ll show you the snippet of MSBuild script you need to insert into your *.csproj file.  Let’s assume you build the DependencyCopy task in a project named MSBuildHelpers.csproj with a namespace of MSBuildHelpers.DependencyCopy and that the MSBuildHelpers.dll assembly is placed in C:\Assemblies.  You’ll want to be running the DependencyCopy task after the build is finished so you would insert the following AfterBuild target right before the ending </Project> tag:

<UsingTask TaskName="BTR.Evolution.MSBuild.DependencyCopy" AssemblyFile="C:\BTR\Source\Assemblies.Evolution\BTR.Evolution.MSBuild.dll" />
<Target Name="AfterBuild">
  <DependencyCopy StartFolder="$(MSBuildProjectDirectory)" ProjectName="$(ProjectName)" Configuration="$(Configuration)" OutputFolder="$(OutputPath)" />
</Target>

After adding that and rebuilding ClientApplication, the following shows that ClassLibrary3* and Newtonsoft* files are correctly copied to the directory as well.

ReleaseAfter

DependencyCopy code…

Finally, the code.  Let me know if you have any questions or comments.  Hopefully this can help you out as much as it has been helping me.

	public class DependencyCopy : Task
	{
		[Required]
		public string StartFolder { get; set; }
		[Required]
		public string ProjectName { get; set; }
		[Required]
		public string Configuration { get; set; }
		[Required]
		public string OutputFolder { get; set; }

		XNamespace ns = "http://schemas.microsoft.com/developer/msbuild/2003";

		public override bool Execute()
		{
			Log.LogMessage( MessageImportance.High, $"Scanning {ProjectName}.csproj for BTR project references/assemblies..." );

			var projectsToScan = GetDependentProjectsToScan().ToArray();

			Log.LogMessage( MessageImportance.High, $"Scanning BTR projects for additional assemblies to publish..." );

			var assembliesToCopy = GetDependentAssemblies( projectsToScan ).ToArray();

			var assemblyFolder = Path.Combine( StartFolder, OutputFolder );

			Log.LogMessage( MessageImportance.High, $"Copy dependent assemblies to {assemblyFolder}..." );

			foreach ( var a in assembliesToCopy )
			{
				var assemblyFile = Path.GetFileName( a );
				var assemblyDestination = Path.Combine( assemblyFolder, assemblyFile.EndsWith( ".resources.dll", StringComparison.InvariantCultureIgnoreCase ) ? Path.GetDirectoryName( a ).Split( '\\' ).Last() : "", assemblyFile );

				if ( !File.Exists( assemblyDestination ) || new FileInfo( a ).LastWriteTime > new FileInfo( assemblyDestination ).LastWriteTime )
				{
					Log.LogMessage( MessageImportance.High, $"\tCopy {a} to {assemblyDestination}." );
					File.Copy( a, assemblyDestination, true );
				}
			}

			return true;
		}

		class DependentProject
		{
			public string AssemblyName { get; set; }
			public string ProjectFile { get; set; }
		}

		private IEnumerable<DependentProject> GetDependentProjectsToScan( string projectFile = null )
		{
			var isRootProject = string.IsNullOrEmpty( projectFile );
			var fileName = projectFile ?? Path.Combine( StartFolder, ProjectName + ".csproj" );
			var project = XElement.Load( fileName );
			var projectFolder = Path.GetDirectoryName( fileName );

			var projectsReturned = new HashSet<string>();

			Func<string, string> getUniqueProjectToScan = f =>
			{
				var projFile = new FileInfo( f ).FullName;

				if ( !projectsReturned.Contains( projFile ) )
				{
					projectsReturned.Add( projFile );
					if ( isRootProject )
					{
						Log.LogMessage( MessageImportance.High, $"\tProject reference: {projFile}" );
					}
					return projFile;
				}
				else
				{
					return null;
				}
			};

			var projectReferences =
				project.Elements( ns + "ItemGroup" )
						.Elements( ns + "ProjectReference" )
						.Select( r => new DependentProject { ProjectFile = new FileInfo( Path.Combine( projectFolder, (string)r.Attribute( "Include" ) ) ).FullName } );

			var btrBinaryReferences =
				project.Elements( ns + "ItemGroup" )
						.Elements( ns + "Reference" )
						.Where( r => ( (string)r.Attribute( "Include" ) ).StartsWith( "BTR." ) )
						.Select( r => (string)r.Element( ns + "HintPath" ) )
						.Select( r =>
							 new DependentProject
							 {
								 AssemblyName = new FileInfo( Path.Combine( projectFolder, r ) ).FullName,
								 ProjectFile = GetProjectFileFromBinaryAssembly( Path.Combine( projectFolder, r ) )
							 }
						);

			var allReferences = projectReferences.Concat( btrBinaryReferences ).ToArray();

			foreach ( var dependentProject in allReferences )
			{
				var projFile = getUniqueProjectToScan( dependentProject.ProjectFile );

				if ( projFile != null )
				{
					yield return dependentProject;

					// Scan for all project references in 'referenced projects'
					foreach ( var s in GetDependentProjectsToScan( projFile ) )
					{
						projFile = getUniqueProjectToScan( s.ProjectFile );

						if ( projFile != null )
						{
							yield return s;
						}
					}
				}
			}
		}

		private string GetProjectFileFromBinaryAssembly( string binaryAssemblyName )
		{
			/*
			* This function is specific to BTR's project structure and location of framework assemblies.
			* We have a structure like the following
			*
			* C:\BTR\Source\Assemblies.Evolution - any binary references to framework assemblies will be
			*										pointed at this directory.
			* C:\BTR\Source\Evolution\ - This is root folder of all framework assemblies.
			* C:\BTR\Source\Evolution\BTR.* - These folders are all the framework projects where the name
			*								   of the folder matches the name of the *.csproj file.
			*
			* Given that structure, the code below can find the *.csproj given a binary assembly found
			* in Assemblies.Evolution.
			*/
			var binaryAssembly = new FileInfo( binaryAssemblyName );
			var projectName = Path.GetFileNameWithoutExtension( binaryAssembly.FullName );
			var sourcePaths = new[] {
				binaryAssembly.Directory.Parent.FullName, // One level up from 'assembly' folder is source for older framework libraries
				Path.Combine(binaryAssembly.Directory.Parent.FullName, "Evolution")
			};

			var projectFileName =
				sourcePaths
					.Select( p => Path.Combine( p, projectName, projectName + ".csproj" ) )
					.First( p => File.Exists( p ) );

			return projectFileName;
		}

		private IEnumerable<string> GetDependentAssemblies( IEnumerable<DependentProject> projectsToScan )
		{
			var assembliesReturned = new HashSet<string>();

			Func<string, bool> tryAddAssemblyToCopy = f =>
			{
				var binaryReference = new FileInfo( f );

				var binaryPath = Path.GetDirectoryName( binaryReference.FullName );
				var binaryFileName = Path.GetFileNameWithoutExtension( binaryReference.FullName );

				var resourceFile = binaryFileName + ".resources.dll";

				var assemblies = new[]
				{
					new { IsDll = true, Assembly = binaryReference },
					new { IsDll = false, Assembly = new FileInfo( Path.Combine( binaryPath, binaryFileName + ".pdb" ) ) },
					new { IsDll = false, Assembly = new FileInfo( Path.Combine( binaryPath, binaryFileName + ".xml" ) ) }
		}.Concat(
					// Make sure to copy all resource files as well
					binaryReference.Directory.GetDirectories()
									.Where( d => File.Exists( Path.Combine( d.FullName, resourceFile ) ) )
									.Select( d => new { IsDll = false, Assembly = new FileInfo( Path.Combine( d.FullName, resourceFile ) ) } )
				).ToArray();

				foreach ( var a in assemblies )
				{
					if ( a.Assembly.Exists && !assembliesReturned.Contains( a.Assembly.FullName ) )
					{
						assembliesReturned.Add( a.Assembly.FullName );
					}
					else if ( a.IsDll )
					{
						// If the dll file doesn't exist, don't even check for the pdb/xml
						return false;
					}
				}

				// if made it this far, I at least added the dll
				return true;
			};

			foreach ( var p in projectsToScan )
			{
				var project = XElement.Load( p.ProjectFile );
				var assemblyDirectory = Path.GetDirectoryName( p.AssemblyName ?? p.ProjectFile );
				var projectDirectory = Path.GetDirectoryName( p.ProjectFile );

				// For the current BTR project, try to include the project assembly from the same Configuration
				// setting, otherwise falling back to the Release mode.  Using AssemblyName in case it was a binary
				// reference, want to grab the files from wherever that is stored.
				var projectAssemblyName = Path.GetFileName( p.AssemblyName ?? (string)project.Elements( ns + "PropertyGroup" ).Elements( ns + "AssemblyName" ).First() + ".dll" );

				if ( string.IsNullOrEmpty( p.AssemblyName ) )
				{
					if ( !tryAddAssemblyToCopy( Path.Combine( assemblyDirectory, "bin", Configuration, projectAssemblyName ) ) )
					{
						tryAddAssemblyToCopy( Path.Combine( assemblyDirectory, "bin", "Release", projectAssemblyName ) );
					}
				}
				else
				{
					tryAddAssemblyToCopy( Path.Combine( assemblyDirectory, projectAssemblyName ) );
				}

				foreach ( var h in project.Elements( ns + "ItemGroup" ).Elements( ns + "Reference" ).Elements( ns + "HintPath" ) )
				{
					tryAddAssemblyToCopy( Path.Combine( projectDirectory, (string)h ) );
				}
			}

			foreach ( var f in assembliesReturned )
			{
				yield return f;
			}
		}
	}

FixedWidthStreamReader, Conceived from Death

The other day, I had the task of converting an function that calculates annuity factors from VBA to C#.  A couple of the function parameters were the desired Male and Female mortality tables.  Thus the pun with the post title (…From Death).

tl;dr – Jump to the code to see how I used Generics, Compiled Expression Trees, and Convert.ChangeType to create my FixedWidthStreamReader.

Given the desired tables, the old VBA code would read in all the rates from text files…120 rows and 2-3 columns in fixed width format.  Not being expert in file streams, I hit Google and ended up at the Stack Exchange post How to read fixed-width data fields in .Net.  You can read the post and the comments, but essentially, the original post required caller to call the following method for each ‘property’ they wanted read.  Additionally, you had to call the properties in the correct order as they appeared on the line since you didn’t pass a position in, but just a size that the reader internally used to keep track of the position.

T Read<T>( int size );

One of the comments suggested mimicking the StructLayoutAttribute which made sense as it keeps the file layout definition neatly packed away as attribute decorations on the output class instead of requiring each client of the reader class to know this definition.

The original poster never changed his code to use attributes and then I received an email from another user who commented on that post (Thanks Giuseppe) and posted his own implementation on the Stack Overflow post Reading data from Fixed Length File into class objects.  Given that inspiration, I decided to update the code with the given goals:

  1. The read<T>( T data ) method immediately jumped out to me as something that I thought I could simplify using Convert.ChangeType.
  2. I wanted to be able to read all the lines of a file and return a list of desired T objects. 
  3. It seemed that if you wanted to read more than one line in the original SO solution, that you would have to manage the parent Stream independently of the FixedLengthReader stream.
  4. I didn’t want to have to have the caller have to instantiate a T object and pass it into the reader.  I wanted the reader to return null or T objects appropriately.

So below is my attempt at a solution.  I derived from StreamReader, but since I really only intend the caller to use ReadLine or ReadAllLines, it may be better to change to just have a private StreamReader inside class.

Additionally, I’m no expert at streams, so not sure how much more performant the Stack Exchange solution’s ReadToBuffer is versus just a simple ReadLine, but here is my code.

FixedWidthStreamReader Code Snippets

Given file of: 
Person 1          1973 100000.54 
Grandparent 1     1950 2000000 

 

This is my type T class that each row in a file represents.  Each field that I want read in is decorated with a Layout attribute describing the start position and the length>

class PersonalInfo
{
   [Layout(0, 20)]
   public string Name;
   [Layout(20, 5)]
   public int YOB;
   [Layout(24, 15)]
   public double Pay;

   public override String ToString() {
       return String.Format("String: {0}; int: {1}; double: {2:c}", Name, YOB, Pay);
   }
}

LayoutAttribute class (note, I should probably support Properties in addition to Fields, but that would be an easy fix).

[AttributeUsage(AttributeTargets.Field)]
class LayoutAttribute : Attribute
{
   public int Index { get; private set; }
   public int Length { get; private set; }

   public LayoutAttribute( int index, int length )
   {
       Index = index;
       Length = length;
   }
}

I will paste FixedWidthStreamReader code below, but here are three usage code scenarios.

1.  Read just the first line of file into a PersonalInfo object.

using( var sr = new FixedWidthStreamReader<PersonalInfo>( @"c:\users\terry.aney\desktop\fixed.txt" ) )
{
	var pi = sr.ReadLine();
	System.Diagnostics.Debug.WriteLine( pi.ToString() );
}

2. Read all lines of file into IEnumerable<PersonalInfo> object.

using( var sr = new FixedWidthStreamReader<PersonalInfo>( @"c:\users\terry.aney\desktop\fixed.txt" ) )
{
	foreach( var pi in sr.ReadAllLines() )
	{
		System.Diagnostics.Debug.WriteLine( pi.ToString() );
	}
}

 

3. Read all lines of a file one at a time into PersonalInfo objects (might want to do this if you want to stop reading when reach someone or something).

using( var sr = new FixedWidthStreamReader<PersonalInfo>( @"c:\users\terry.aney\desktop\fixed.txt" ) )
{
	PersonalInfo pi;
	while( ( pi = sr.ReadLine() ) != null )
	{
		System.Diagnostics.Debug.WriteLine( pi.ToString() );
	}
}


Finally, here is the FixedWidthStreamReader class.  I create compiled Lambda expressions for each Field on the object and call the compiled Action during ReadT()

public class FixedWidthStreamReader<T> : StreamReader where T : class
{
	private List<Tuple<int, int, Action<T, string>>> propertySetters = new List<Tuple<int, int, Action<T, string>>>();
	
	public FixedWidthStreamReader( Stream stream ) : base( stream ) { GetSetters(); }
	public FixedWidthStreamReader( string path ) : base( path ) { GetSetters(); }
	
	private void GetSetters() 
	{
		var myType = typeof( T );
		var instance = Expression.Parameter( myType );
		var value = Expression.Parameter( typeof( object ) );
		var changeType = typeof( Convert ).GetMethod( "ChangeType", new[] { typeof( object ), typeof( Type ) } );

		/* Should probably do Properties here too, if I change AttributeUsage for LayoutAttribute I would */
		foreach ( var fi in myType.GetFields() )
		{
			var la = fi.GetCustomAttribute<LayoutAttribute>();
			if ( la != null )
			{
				var convertedObject = Expression.Call( changeType, value, Expression.Constant( fi.FieldType ) );

				var setter = Expression.Lambda<Action<T, string>>(
					Expression.Assign( Expression.Field( instance, fi ), Expression.Convert( convertedObject, fi.FieldType ) ),
					instance, value
				);
				
				var prop = setter.Compile() as Action<T, string>;
				propertySetters.Add( Tuple.Create( la.Index, la.Length, prop ) );
			}
		}
	}

	public new T ReadLine()
	{
		if ( Peek() < 0 ) return (T)null;
		
		return ReadT( base.ReadLine() );
	}
	
	private T ReadT( string line )
	{
		if ( string.IsNullOrEmpty( line ) ) return null;
		
		var t = Activator.CreateInstance<T>();

		foreach( var s in propertySetters )
		{
			var l = line.Length;
			
			if ( l > s.Item1 )
			{
				s.Item3( t, line.Substring( s.Item1, Math.Min( s.Item2, l - s.Item1 ) ).Trim() );
			}
		}
		return t;
	}
	
	public IEnumerable<T> ReadAllLines()
	{
		string line = null;
		
		while ( !string.IsNullOrEmpty( ( line = base.ReadLine() ) ) )
		{
			yield return ReadT( line );   
		}
	}
}

 

Ultimately, I used this as a VERY SIMPLE learning experience on how to create compiled Expression Trees, which I still need to mess with more to fully understand, but hopefully this will give you both a simple introduction to that along with a pretty functional FixedWidthStreamReader.

Use SelectExcept When You Are Too Lazy To Type

Do you love LINQPad?  Use it every day?  Do ad-hoc queries non-stop against your DataContexts?  Love C#/LINQ’s ability for anonymous object projections into new forms?  I’m assuming you said yes to all those.  Here is the question that drives this post…do you hate having to type *every* field *except* one (or a few) when you *only* need almost all fields?  If you answered yes to that last question or are wondering why I don’t just allow the entire row to be selected/returned, continue reading…

Continue reading “Use SelectExcept When You Are Too Lazy To Type”

L2S Extensions…MVC style

Four years later…I’ve finally come up for breath.  I hope to start a better pattern of trying to get some technical posts created once in a while.  Until I have my first inspiration – that I feel would be beneficial to anyone in the WWW, I thought I’d provide a little update to my L2S Extensions.

Here is what has changed since my last post 4 years ago.  I’m going to make the assumption that since there was really only three new improvements, that the code most have been pretty solid Smile.  Some of the changes below were due to the fact that the main code I generate for my company has shifted to the ASP.NET MVC framework and have started leveraging the System.ComponentModel.DataAnnotations namespace to decorate pretty much everything.  Since I was querying some of those same classes, it made sense to update my L2S Extensions code to obey some attributes as well.  All the changes below occur in DumpCSV().

Continue reading “L2S Extensions…MVC style”

LINQ to SQL Batch Updates/Deletes: Fix for ‘Could not translate expression’

I’ve found and posted a new fix in the code from my original post: Batch Updates and Deletes with LINQ to SQL.  I’m not sure of the etiquette for this sort of thing: new post (like I’m doing) or just a comment in the original post.  But since I did get a fair amount of hits to the article but minimal comments, people who may have downloaded the code wouldn’t get an update notification and I want to be sure to make them aware of an issue/fix (assuming they are monitoring via a RSS feed).

But wait, there’s more, click to keep reading…