15

I'm using the RazorEngine library (http://razorengine.codeplex.com/) in an MVC 3 web application to parse strings (that aren't views) using the Razor templating language.

In general, this works fine. However, when multiple users are accessing code that parses Razor templates at the same time, I occasionally see errors that look like they occur in the internal Razor compiler (see two of them below). I'm having trouble interpreting these errors, but my guess is that the way I'm invoking the Razor compiler is not concurrency safe.

Is this a known issue with the Razor compiler? How do normal Razor views (.cshtml) not run into this problem? Is there a workaround for this better than wrapping all of my application's calls to Razor.Parse in a mutex?

My calling code is as follows, just a simple wrapper around Razor.Parse:

    protected string ParseTemplate<T>(string templateString, T model)
    {
        //This binderAssembly line is required by NUnit to prevent template compilation errors
        var binderAssembly = typeof(Microsoft.CSharp.RuntimeBinder.Binder).Assembly;
        var result = Razor.Parse(templateString, model);
        return result;
    }

Error one:

System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: chunkLength
at System.Text.StringBuilder.ToString()
at System.Web.Razor.Generator.RazorCodeGenerator.BlockContext.MarkEndGeneratedCode()
at System.Web.Razor.Generator.RazorCodeGenerator.WriteBlock(BlockContext block)
at System.Web.Razor.Parser.ParserContext.FlushNextOutputSpan()
at System.Web.Razor.Parser.ParserContext.StartBlock(BlockType blockType, Boolean outputCurrentBufferAsTransition)
at System.Web.Razor.Parser.ParserBase.ParseComment()
at System.Web.Razor.Parser.ParserBase.TryParseComment(SpanFactory previousSpanFactory)
at System.Web.Razor.Parser.ParserBase.ParseBlockWithOtherParser(SpanFactory previousSpanFactory, Boolean collectTransitionToken)
at System.Web.Razor.Parser.HtmlMarkupParser.TryStartCodeParser(Boolean isSingleLineMarkup, Boolean documentLevel)
at System.Web.Razor.Parser.HtmlMarkupParser.ParseRootBlock(Tuple`2 nestingSequences, Boolean caseSensitive)
at System.Web.Razor.Parser.RazorParser.Parse(LookaheadTextReader input, ParserVisitor visitor)
at System.Web.Razor.RazorTemplateEngine.GenerateCodeCore(LookaheadTextReader input, String className, String rootNamespace, String sourceFileName, Nullable`1 cancelToken)
at System.Web.Razor.RazorTemplateEngine.GenerateCode(TextReader input, String className, String rootNamespace, String sourceFileName, Nullable`1 cancelToken)
at System.Web.Razor.RazorTemplateEngine.GenerateCode(TextReader input)
at RazorEngine.Compilation.CompilerServiceBase.GetCodeCompileUnit(String className, String template, ISet`1 namespaceImports, Type templateType, Type modelType)
at RazorEngine.Compilation.DirectCompilerServiceBase.Compile(TypeContext context)
at RazorEngine.Compilation.DirectCompilerServiceBase.CompileType(TypeContext context)
at RazorEngine.Templating.TemplateService.CreateTemplate(String template, Type modelType)
at RazorEngine.Templating.TemplateService.Parse[T](String template, T model, String name)
at RazorEngine.Razor.Parse[T](String template, T model, String name)

Error two:

System.ObjectDisposedException: Cannot read from a closed TextReader.
at System.IO.StringReader.Read()
at System.Web.Razor.Text.BufferingTextReader.NextCharacter()
at System.Web.Razor.Text.BufferingTextReader.Read()
at System.Web.Razor.Parser.ParserContext.AcceptCurrent()
at System.Web.Razor.Parser.HtmlMarkupParser.ParseRootBlock(Tuple`2 nestingSequences, Boolean caseSensitive)
at System.Web.Razor.Parser.RazorParser.Parse(LookaheadTextReader input, ParserVisitor visitor)
at System.Web.Razor.RazorTemplateEngine.GenerateCodeCore(LookaheadTextReader input, String className, String rootNamespace, String sourceFileName, Nullable`1 cancelToken)
at System.Web.Razor.RazorTemplateEngine.GenerateCode(TextReader input, String className, String rootNamespace, String sourceFileName, Nullable`1 cancelToken)
at System.Web.Razor.RazorTemplateEngine.GenerateCode(TextReader input)
at RazorEngine.Compilation.CompilerServiceBase.GetCodeCompileUnit(String className, String template, ISet`1 namespaceImports, Type templateType, Type modelType)
at RazorEngine.Compilation.DirectCompilerServiceBase.Compile(TypeContext context)
at RazorEngine.Compilation.DirectCompilerServiceBase.CompileType(TypeContext context)
at RazorEngine.Templating.TemplateService.CreateTemplate(String template, Type modelType)
at RazorEngine.Templating.TemplateService.Parse[T](String template, T model, String name)
at RazorEngine.Razor.Parse[T](String template, T model, String name)
Edward McTighe
  • 153
  • 1
  • 1
  • 7

1 Answers1

16

Update: According to a blog post from their team, the latest version 3.x (on Github) is now thread-safe. I have not vetted the veracity of its thread-safety, but assume it has been implemented properly. Please consider the rest of this answer useful only for historical purposes.


Judging from the code, this project doesn't look remotely thread-safe.

Razor.Parse:

public static string Parse<T>(string template, T model, string name = null)
{
    return DefaultTemplateService.Parse<T>(template, model, name);
}

TemplateService.Parse:

public string Parse<T>(string template, T model, string name = null)
{
    var instance = GetTemplate(template, typeof(T), name);
    ...
}

TemplateService.GetTemplate:

internal ITemplate GetTemplate(string template, Type modelType, string name)
{
    if (!string.IsNullOrEmpty(name))
        if (templateCache.ContainsKey(name))
            return templateCache[name];

    var instance = CreateTemplate(template, modelType);

    if (!string.IsNullOrEmpty(name))
        if (!templateCache.ContainsKey(name))
            templateCache.Add(name, instance);

    return instance;
}

So, Razor.Parse is a static method. DefaultTemplateService is a static property on Razor, and Parse and GetTemplate are instance methods, but effectively invoked statically because of the static DefaultTemplateService. This means all threads go through the same instance and go through GetTemplate. You'll notice that GetTemplate mutates state (templateCache) without acquiring any locks. Therefore, this code is not threadsafe.

Kirk Woll
  • 76,112
  • 22
  • 180
  • 195
  • I see that the RazorEngine code is doing non-threadsafe things, but I'm not sure if that's actually affecting my code. From looking at the code, the particular instance of non-threadsafe operations you posted is bypassed when name is null -- and the actual errors all seem to occur once we get to `System.Web.Razor`, after the `RazorEngine` code. – Edward McTighe Jun 22 '11 at 20:01
  • Actually, my point was rather that the entire project is probably not thread-safe. In general, if you see *one* example of non-threadsafe code, then it's very likely the developer was not well prepared to deal with multiple threads throughout the rest of the codebase at the outset. – Kirk Woll Jun 22 '11 at 20:16
  • For example, `DirectCompilerServiceBase` has the method `Compile` which is in your second stack trace. **One** instance of `DirectCompilerServiceBase` is used for all invocations. (it is an instance field on the static `TemplateService`) `DirectCompilerServiceBase` has an *instance* field holding a `CodeDomProvider`. The `CodeDomProvider` is **not** threadsafe. Remember, the rule of the thumb when dealing with multi-threaded situations is that by default **all classes should be presumed non-threadsafe**. In almost every case where a class has a mutable field, it's not threadsafe. – Kirk Woll Jun 22 '11 at 20:19
  • @Edward, I repeat what I said at the outset: **This project doesn't look remotely thread-safe.** – Kirk Woll Jun 22 '11 at 20:21
  • Thank you, that's very helpful. I will treat `RazorEngine` as non-threadsafe and work out how the calling code in the application should deal with that fact. – Edward McTighe Jun 22 '11 at 20:44
  • 3
    The reason you don't see these issues with Razor in MVC, is that the Razor compiler is invoked once per request, and each request is served by a single thread, so the concurrency issues you see don't occur. – Matthew Abbott Jun 23 '11 at 15:21
  • @MatthewAbbott, that sounds great, but by all appearances, the project appears dead. – Kirk Woll Jan 16 '13 at 06:10
  • 1
    You may want to check Nuget then, we pushed v3.1.0 but a few days ago... and this is quite an old question now.? – Matthew Abbott Jan 16 '13 at 09:17
  • @Matthew, it's an old question, but still gets views. The link in the question is pointing to the old codeplex site, and I had missed that it mentions the move to github. Thanks for letting me know. – Kirk Woll Jan 16 '13 at 14:40