Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"ExceptionMessage":"The same key was already used for another template!" #232

Closed
mikeandersun opened this issue Feb 2, 2015 · 21 comments
Closed
Labels

Comments

@mikeandersun
Copy link

I encourage with error when duplicate a page in another tab in the browser.
Any point to resole it will be appreciated.

@matthid
Copy link
Collaborator

matthid commented Feb 2, 2015

It looks like you used the same template key (/name) for multiple different templates, this is not supported.
Use:

RunCompile(template1, "name1", typeof(Model), model)
RunCompile(template2, "name2", typeof(Model), model)

Or even better implement your own name -> template resolving strategy by implementing ITemplateManager. Then you can use the templates by name:

RunCompile("name1", typeof(Model), model)
RunCompile("name2", typeof(Model), model)

@Spaceman1861
Copy link

Hi,

I have run into a similar issue when updating from 3.2.

I was using Razor.Parse() which seems to check if its compiled and if not compile or update it.

I wish to do that without calling Razor.Parse().

My reasoning is that my templates can be changed will the application is running from within the application so when a user updates the cshtml i need to reload it upon next request with the changes.

How is this possible in the current version? Btw awesome code. :)

@matthid
Copy link
Collaborator

matthid commented Feb 3, 2015

It is. What you want to do is provide your own ICachingProvider implementation. You can take the one from the RazorEngine codebase and edit it to your needs. You can now even recompile a template file when it was changed and silently replace it, so the next Run or RunCompile call will use it (without impacting the application performance while compiling).

If you manage to write a generic caching approach which others might find useful, I'd love to see that integrated in RazorEngine.

Hope this helps, if you have any problems with this approach feel free to ask.

@Spaceman1861
Copy link

Hey,

Here is my solution for anyone wanting to update specific templates on the fly.

I created my own itemplate manager which almost a straight copy from delegate.

I changed the following code

/// <param name="source">the source-code of the template</param>
public void AddDynamic(ITemplateKey key, ITemplateSource source)
{
    _dynamicTemplates.AddOrUpdate(key, source, (k, oldSource) =>
    {
        // This code is from the original template manager implementation.
        //if (oldSource.Template != source.Template)
        //{
            //source.Template 
            //throw new InvalidOperationException("The same key was already used for another template!");
        //}
        return source;
    });
}

And in the cases where i need to update i know to call compile in the code. Otherwise i call run. All is well. :)

Hope this helps someone.

@matthid
Copy link
Collaborator

matthid commented Feb 5, 2015

Just for reference: Yes this works, but the reason the commented code is there is because without it you get weird behavior in the RunCompile and Run methods calls! For example when you call RunCompile with a new template source-code it implicitly calls AddDynamic and then uses the (outdated) cache. We throw a exception so you can catch such (subtle) situations.

I would not recommend depending on this specific behavior. It is possible that future versions throw on Compile when there is already a cached item present (which your solution depends on).

Instead I would encourage you to add an event to your ITemplateManager implementation and let a custom ICachingManager implementation depend on this event and invalidate the cache.

The current ITemplateManager and ICachingManager implementations are really not completely bullet proof (and thought-through) when it comes to cache invalidation and changing templates. So it is always the best in such situations to implement your own versions (as requirements are so different). However patches with more general implementations are welcome :) .

@Spaceman1861
Copy link

I agree its not the best i just needed a quick fix. I dont need to invalidate the entire cache just the template in question. Its interesting to me so i might take a look at implementing a solution more along the lines you suggest.

@waynebrantley
Copy link

I believe there is still an issue around this. The following code throws the exception above. (I realize this causes a slow memory leak)

            var cacheProvidxer=new InvalidatingCachingProvider();
            var config = new TemplateServiceConfiguration();
            config.CachingProvider = cacheProvidxer;
            var service = RazorEngineService.Create(config);
            Engine.Razor = service;

            Engine.Razor.RunCompile("Some Text", "SomeKey", null, new { IsAuthenticated = true });
            cacheProvidxer.InvalidateAll();
            Engine.Razor.RunCompile("Some Different Text", "SomeKey", null, new { IsAuthenticated = true });

Is there something wrong with the above code? (Uses version 3.7.0)

@matthid
Copy link
Collaborator

matthid commented Jul 4, 2015

The problem is that you only invalidate the Compile Cache and not the Template Manager.

The following would work and compile the template twice (note that this leaks as you already noted):

            var cacheProvidxer=new InvalidatingCachingProvider();
            var config = new TemplateServiceConfiguration();
            config.CachingProvider = cacheProvidxer;
            var service = RazorEngineService.Create(config);
            Engine.Razor = service;

            Engine.Razor.RunCompile("Some Text", "SomeKey", null, new { IsAuthenticated = true });
            cacheProvidxer.InvalidateAll();
            Engine.Razor.RunCompile("SomeKey", null, new { IsAuthenticated = true });

To get what you want you either need to use the DelegateTemplateManager (which has a RemoveDynamic method you need to call additionally to InvalidateAll). Or write your own CachingProvider-Wrapper and TemplateManager to directly connect them (so when InvalidateAll is called your Manager is cleared as well).

@waynebrantley
Copy link

This whole caching and template manager stuff seems to be overly complex. Was hoping to simply read some values from a database and compile them..when they change..clear the cache. But it seems you have to write a template manager and a caching provider, etc. Most examples are around a 'file system'..do people use a file system a lot when using this product?

I will try this DelagateTemplateManager....but seems such overkill. I guess it is all around you trying to 'resolve references' to other templates inside the RunCompile method.

@matthid
Copy link
Collaborator

matthid commented Jul 4, 2015

Most of the Complexity is a direct result of several CLR limitations we run into all the time:

Sorry but AFAIK there is no way to make a simple API that "just works" (at least I cannot see it). It always boils down to carefully look at your performance/environment requirements and build a solution/workaround you can live with. Once you are at this point you can see why the complexity is necessary to get the perfect implementation you are searching for.

I would love to be proven wrong, just fix everything and send a pull request :). One thing to keep in mind is that we should provide a API that fails as fast as possible and not allow users to do things that will fail once the memory runs out...

Most examples are around a 'file system'..do people use a file system a lot when using this product?

I don't know. I do and it makes sense to save templates in files to have a hierarchical structure...

@etherni
Copy link

etherni commented Aug 6, 2015

+1 for Caching/Template Manager overkill.

We have an app where templates used for emails, letters and memos stored in the DB. They also presented to the user, so some users can change templates on-the-fly.
So, as far as I understand - even if I will go through code and update all methods to use Template Id in addition to the template or will find other means to "fake" template Id - there is no way to update or remove template from cache other than re-create cache provider?
Is there an example on how to handle situation when you need to update particular template?

Another question, which I might miss finding answer for - how to implement logic of either adding template to the manager or using template id?

return Engine.Razor.RunCompile(razorTemplate, templateId, null, Model);

Above code will fail if I will try to run it for the second time with the same template Id/template.
Is there an example available how to implement "add if missing/otherwise use cached template" logic?

@matthid
Copy link
Collaborator

matthid commented Aug 7, 2015

@asinelnikov

They also presented to the user, so some users can change templates on-the-fly.

The exception is exactly for your case, because if it would "just work" out of the box you would run out of memory or into some other weird and hard to debug issues. So it works as designed by showing you: "You have a problem!". Maybe the message itself could be improved ...

You are exactly in the case where you need to think about memory and assembly unloading (the only exception is when your application is short-lived). Once you figured out a solution you can easily use one of the existing implementations if they fit your needs: https://antaris.github.io/RazorEngine/TemplateManager.html . We have the InvalidatingCachingProvider and in your case you would need a custom ITemplateManager which resolves from the database and eventually invalidates the cache if something changes (always keep in mind that this is a memory leak -> recycle the AppDomain from time to time!).

I don't think implementing one interface ITemplateManager (where you effectively only need to implement Resolve only, because the example implementations are fine most of the time) is overkill for your scenario. But feel free to suggest new ideas how the experience could be improved.

@etherni
Copy link

etherni commented Aug 7, 2015

Thank you for the quick response!

Our application while being moderate in size actually used mostly by one user at a time and not continuously. That is why I guess we never experienced memory leaking issues.

I guess I can follow this guide.
Would you be so kind to check it and confirm that it is "intended" way of using RazorEngine?

Thank you.

@matthid
Copy link
Collaborator

matthid commented Aug 7, 2015

You can completely reset the cache easily by using a new IRazorEngineService instance (Konstantin Smolyakov) or refreshing the DefaultCachingProvider (TugboatCaptain).
But please note that this DOES NOT free memory from already loaded templates (because loaded assemblies cannot be unloaded)! And it does throw away the whole cache and recompile and re-load templates that are already loaded in memory, i.e. unmodified templates (which is a waste of CPU time and memory)

And none of these answers actually answer the question which specifically talk about the memory leak. In fact the only possible and correct answer is that you need to unload the AppDomain to free the memory (none of the answers actually free memory, they "just work" by consuming more!). Once you realize how nasty this problem is, you can think about your memory requirements and how long you can keep the current AppDomain before recycling.

Once you reached this point the "intended" way of course would be to write your own ICachingProvider to realize the solution you need (to communicate how much templates are currently cached/staled with another AppDomain).
Or realize that leaking memory is not really a problem in your application and use the InvalidatingCachingProvider. Use it in connection with your own ITemplateManager that invalidates the cache when required (see https://github.com/Antaris/RazorEngine/blob/master/src/source/RazorEngine.Core/Templating/WatchingResolvePathTemplateManager.cs#L34 for an example).

Hope this helps.

@tofutim
Copy link

tofutim commented Apr 20, 2016

How do I replace a template? Assuming I don't mind the leaking memory? When I try to replace it by recompiling I get 'The same key was already used for another template!'. I expect to do this once in a couple blue moons with the server resetting every few days.

@rsuliteanu
Copy link

Hi, I'm pretty new to this. I just want a template engine that I can use to send emails with some dynamic database content. So, my app is running in production and let's say I want to fix a typo in the template. I FTP the new template file to the server and instead of it just working, I get this error. That doesn't make sense. I don't want to change the name of the key and re-compile and re-publish my whole app! I just want to make a change to my template and have a happy day.

Could you please explain why this VERY common scenario doesn't work out of the box? Is there a SIMPLE solution?

Many thanks,
Ron

@leedavi
Copy link

leedavi commented Aug 2, 2016

I agree with Ron. This is a VERY common scenario and there should be a way to clear this template out of memory without the need to recreate an instance and create a memory leak.

A simple RemoveChachedTemplate method on the razor engine would solve the problem for most people.

I see the IsTemplateCached Method, but it's hardly any use to know you've got a cached template without the ability to remove that single cached template. the only option I have is to create a memory leak for altered templates, which is not a good solution.

Regards,
Dave.

@simoazzo
Copy link

Hi @leedavi ,

Are there any updates on this please, where you can remove chached template or else delete all templates in cache?

@Elven11
Copy link

Elven11 commented Jan 26, 2017

Why aren't we allowed to just parse the template, without involving caching at all?
For most of us, this would solve our problems.

@leedavi
Copy link

leedavi commented Jan 29, 2017

@simoazzo,

In the end I found an idea of using a MD5Hash, that someone posted as a solution. (Sorry not to give praise to the guy who had the idea, I can';t find the post where I found the idea,)

Here's the code I use in my NBrightBuy project. You'll obviously need to alter it to match your environment. It does cause a duplicate in memory when you edit the template, but in my standard situation I only alter templates during development, so this works fine.

      public static string RazorRender(Object info, string razorTempl, string templateKey, Boolean debugMode = false)
        {
            var result = "";
            try
            {
                var service = (IRazorEngineService)HttpContext.Current.Application.Get("NBrightBuyIRazorEngineService");
                if (service == null)
                {
                    // do razor test
                    var config = new TemplateServiceConfiguration();
                    config.Debug = debugMode;
                    config.BaseTemplateType = typeof(NBrightBuyRazorTokens<>);
                    service = RazorEngineService.Create(config);
                    HttpContext.Current.Application.Set("NBrightBuyIRazorEngineService", service);
                }
                Engine.Razor = service;
                var israzorCached = Utils.GetCache("nbrightbuyrzcache_" + templateKey); // get a cache flag for razor compile.
                if (israzorCached == null || (string)israzorCached != razorTempl)
                {
                    result = Engine.Razor.RunCompile(razorTempl, GetMd5Hash(razorTempl), null, info);
                    Utils.SetCache("nbrightbuyrzcache_" + templateKey, razorTempl);
                }
                else
                {
                    result = Engine.Razor.Run(GetMd5Hash(razorTempl), null, info);
                }

            }
            catch (Exception ex)
            {
                result = ex.ToString();
            }

            return result;
        }

        /// <summary>
        /// work arounf MD5 has for razorengine caching.
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        private static string GetMd5Hash(string input)
        {
            var md5 = MD5.Create();
            var inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
            var hash = md5.ComputeHash(inputBytes);
            var sb = new StringBuilder();
            foreach (byte t in hash)
            {
                sb.Append(t.ToString("X2"));
            }
            return sb.ToString();
        }

@fusion27
Copy link

Our application is going to be quite busy, we require caching. We'll have 3 templates when we move to production, that number will grow to ~6 in the coming year. My 'skull thickness' issue was that my stubborn brain wanted to name & add the template every time. If it's already cached, the solution was to not add it.

Controller

string razorTemplateName = "Layout " + sm.name;
bool isTemplateCached = Engine.Razor.IsTemplateCached(razorTemplateName, null);

if (!isTemplateCached) {
    var layoutSource = new LoadedTemplateSource(File.ReadAllText(Path.Combine(@"Templates\", sm.layoutName)));
    Engine.Razor.AddTemplate(razorTemplateName, layoutSource);
}

View

@{ 
    string layoutName = "Layout " + Model.sm.name;
    Layout = layoutName;
}

Thanks for your hard work @matthid!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests