If you run your application, those TagHelper’s won’t work.
This is because you don’t have any @addTagHelper directive yet in your razor view, and so razor doesn’t know it should be using them. This is where things get a bit interesting!
Let’s add an addTagHelper directive
So we add the directive to our __ViewImports.cshtml file:
This is because by defualt MVC does not resolve TagHelper assemblies registered with the parts system (atleast this is true as of RTM 1.0.0) so it complains when it processes that directive, saying it can’t find such an assembly - because it can only see assemblies that are in the bin folder by default. So it can’t see your plugin assembly.
How do we solve?
Well if you add this line:
1
mvcBuilder.AddTagHelpersAsServices();
That will register some replacement services that will check the application parts system when trying to resolve the tag helper assemblies based on the name provided by the addTagHelper directive.
However - this now works but it’s still not ideal because we still have to add a directive for each plugin before it will work on our page/s. So when someone develops a new plugin, it won’t work until we modify our _ViewImports.cshtml file and add another line:
This can be incredibly frustrating because if you are wanting an extensibile system where plugins can be installed on the fly, then they should just work without constant modifications to source code.
So Can We Do Better?
Yup. So here is my solution to this issue, and that is to allow globbing to be supported in the addTagHelper directive for the assembly name, just like it is for the TypeName portion.
So this is how you do that.
ITagHelperTypeResolver
We need to create an ITagHelperTypeResolver and implement it’s Resolve method. This method takes the string provided by in the addTagHelper directive and returns all TagHelper type’s that are matches to that string. We will make our implementation support globbing on the assembly name so it can match TagHelper types accross multiple assemblies registered with the Application Parts system, instead of just from a single one.
Here is my quick and dirty implementation, where I took a lot of the code from the microsoft implementation, and just added a few tweaks for globbing:
publicclassAssemblyNameGlobbingTagHelperTypeResolver:ITagHelperTypeResolver{privatestaticreadonlySystem.Reflection.TypeInfoITagHelperTypeInfo=typeof(ITagHelper).GetTypeInfo();protectedTagHelperFeatureFeature{get;}publicAssemblyNameGlobbingTagHelperTypeResolver(ApplicationPartManagermanager){if(manager==null){thrownewArgumentNullException(nameof(manager));}Feature=newTagHelperFeature();manager.PopulateFeature(Feature);// _manager = manager;}/// <inheritdoc />publicIEnumerable<Type>Resolve(stringname,SourceLocationdocumentLocation,ErrorSinkerrorSink){if(errorSink==null){thrownewArgumentNullException(nameof(errorSink));}if(string.IsNullOrEmpty(name)){varerrorLength=name==null?1:Math.Max(name.Length,1);errorSink.OnError(documentLocation,"Tag Helper Assembly Name Cannot Be Empty Or Null",errorLength);returnType.EmptyTypes;}IEnumerable<TypeInfo>libraryTypes;try{libraryTypes=GetExportedTypes(name);}catch(Exceptionex){errorSink.OnError(documentLocation,$"Cannot Resolve Tag Helper Assembly: {name}, {ex.Message}",name.Length);returnType.EmptyTypes;}returnlibraryTypes;}/// <inheritdoc />protectedIEnumerable<System.Reflection.TypeInfo>GetExportedTypes(stringassemblyNamePattern){if(assemblyNamePattern==null){thrownewArgumentNullException(nameof(assemblyNamePattern));}varresults=newList<System.Reflection.TypeInfo>();for(vari=0;i<Feature.TagHelpers.Count;i++){vartagHelperAssemblyName=Feature.TagHelpers[i].Assembly.GetName();if(assemblyNamePattern.Contains("*"))// is it actually a pattern?{if(tagHelperAssemblyName.Name.Like(assemblyNamePattern)){results.Add(Feature.TagHelpers[i]);continue;}}// not a pattern so treat as normal assembly name.varassyName=newAssemblyName(assemblyNamePattern);if(AssemblyNameComparer.OrdinalIgnoreCase.Equals(tagHelperAssemblyName,assyName)){results.Add(Feature.TagHelpers[i]);continue;}}returnresults;}privateclassAssemblyNameComparer:IEqualityComparer<AssemblyName>{publicstaticreadonlyIEqualityComparer<AssemblyName>OrdinalIgnoreCase=newAssemblyNameComparer();privateAssemblyNameComparer(){}publicboolEquals(AssemblyNamex,AssemblyNamey){// Ignore case because that's what Assembly.Load does.returnstring.Equals(x.Name,y.Name,StringComparison.OrdinalIgnoreCase)&&string.Equals(x.CultureName??string.Empty,y.CultureName??string.Empty,StringComparison.Ordinal);}publicintGetHashCode(AssemblyNameobj){varhashCode=0;if(obj.Name!=null){hashCode^=obj.Name.GetHashCode();}hashCode^=(obj.CultureName??string.Empty).GetHashCode();returnhashCode;}}}
Now we just register this on startup, after we have registered MVC:
Now we can just add one directive to our __ViewImports.cshtml file, like this:
1
@addTagHelper"*, Plugin.*"
Now that will include all TagHelpers that live in assemblies matching that glob. We can drop new plugins in and their tag helpers will light up automatically.