Pushing Umbraco to the limits
New Heroes is an online softskill learning platform which a large dev team has been working on fulltime for over 9 months.
It uses pretty much every api in the core and in this session we'll explain what techniques we've used to build this platform.
View transcript
Good afternoon everybody. Can you hear me? Hello? Yep. Yes. Okay. So, people standing in the back, there are some seats left here on the front if you want to sit. I don't know if the sound is loud enough for that video. We'll find out in a minute. Okay. That's technical. Maybe this? Yeah. That's better. Okay, yeah. What I was saying, I work at a company called Colors in the city of Den Bosch in the Netherlands. Just got announced Umbraco MVP today. And I'm working with Umbraco since late 2007, early 2008. It was still version 3 at the moment. Yeah. My name is Jeroen Breuer. I'm a senior developer at Colors. I've been an Umbraco MVP three times. I've been using Umbraco since 2009. Last year at CodeGarden, I was actually still looking for a new job. And since then, I've only been working on this project full time. And we're still looking for other devs to join our team. So, we prepared a little agenda. Are you hearing me? We prepared a little agenda for today. First, we are going to explain to you what New Heroes is. Show you some statistics. Show you something about the load balance setup. And how you can do that with Umbraco. One-on-one multilingual example. How we did our database access and also our database maintenance. We'll show you some stuff, what you can do with the mini-profiler that ships with Umbraco. Some other tips for performance tweaking. Something called Plugin Manager and Object Resolvers. There's a core API. And you can ask us some questions if you have any. But maybe I can tell you something about New Heroes first. New Heroes is an online platform for taking trainings and soft skills like leadership, communication. They even have a training how to speak relaxed for a big group. So, that was useful for today. So, and everything is done online for a small monthly fee. You can subscribe. You can start for one euro. And after that, it's €9.95 each month, I think. And you can cancel your subscription anytime. But we have a small video to show you what New Heroes actually is. One small remark. So, in this case, the content is the product. For example, the Carlsberg website. The website, the product is the beer. And that's what the website is about. But in this case, people pay a subscription to see the content which we've entered into in Braco. Now, first, a small video. It's Dutch with English subtitles. Welcome to New Heroes. You want to learn something, otherwise you wouldn't be here. For example, finally, to bring some bad news to your leadership. But yes, how do you do that? Time to get started. How should I put it? But wait a minute. Learning takes time and energy. So, do you really want that? Yes? Then you're ready to set your goals. Goals give you focus. They keep you sharp and give you a boost in the back. So you're already on the go. Time for some theory and the first exercises. I think it wasn't that much. Do you come across something interesting that you want to keep on your dashboard? Then bookmark it immediately. Where and when you start, it doesn't matter. As long as it's a pleasant moment and a pleasant place for yourself. The first exercises. The second exercises. The third exercises. And you start practicing. And what does that mean on your car? Hearts? Well, I actually thought that... Safe on the platform. By sharing your insights, experiences and videos with your supporter. This is your own hero, from whom you can learn a lot. Like your partner, colleague or friend. And if you can't choose, you can always invite more supporters. The first exercises. In easy situations. Yes, the stairs. And difficult situations. Yes, hey mom. I have to tell you something annoying. Until you're ready for the most important step. To achieve your goal. The first exercises. The fourth exercises. So... What do you want to learn? There are nested content, Vorto, new pickers and the multi URL picker. I don't know if Handy, Lee and Matt are here, but I'd like to thank them. Because without... With their packages we barely needed to create custom property editors anymore. We also use some other community tools. USync, the Diplo Trace Log Viewer. CMS Import. Bulk Manager. Load Balancing Dashboard. Models Builder. And the Core Value Converters. And for this project we actually only needed to build one custom property editor. It's for managing short URLs. So if you create a short code, you visit that URL, you get redirected to the correct node. If it's used somewhere else, you get a message. It's a bit like the unique property from uComponents. And that's the only thing we needed a custom build for. Yeah. And then sometimes... Do you hear me, everybody? Yeah, okay. So we started on Braco 7.3. I think the first beta that was out. Because we needed the new flexible load balancing. It also helped iron out some bugs in the flexible load balancing. So... And some other numbers are... We have around 1300 media items. 8500. I think at the moment it's actually more because these are stats from last week. I think it's about 9000 content items now. And we have 37000 cache instructions in the database. We'll get back later to that when I explain the load balancing, what that is. And we have 205 doc types in this project. And now some interesting stuff. Okay. So I deleted the wrong document type two times. I actually first deleted it on the dev environment. Then I went to the QA environment to copy it back. Then also deleted it on the QA environment accidentally. And... Yeah, we had a lot of test content that we lost because of that. But luckily we've got USYNC. So it was still easy to import the document type again. As a development team, we also set some goals. One was that the application should autoscale. Because in the future, New Heroes is very ambitious. They want to be the Netflix for online training on soft skills. So they have to handle a huge amount of visitors. So the application should autoscale if they have a lot of traffic. We wanted to keep the custom components as limited as possible. So we use core functionality where possible. We had a big focus on performance because we need to handle a lot of visitors. And because it's a training platform, everything happens online. We cannot do a lot of output caching and stuff. So everything is very dynamic. So performance was very important. And then we also want to keep everything very modular. So also the front end is set up very modular. But the atomic design principle. And we use a tool, Catering Lab, for that. Unfortunately, we didn't have room in this presentation to show you that. But if you have questions, come find us later and we show you that. And then one of the things was also they wanted to manage some content. One-on-one, multi-language. And then I already talked a little bit about the load balancing. That's why we started on Umbraco 7.3 beta because they introduced the new flexible load balancing. If you have done load balancing before 7.3, you had to go into the Umbraco settings config file and add your servers there manually. But if you want to autoscale, yeah, that is not going to work because you have to write custom codes to update all the config files on all the servers. So we needed the flexibility. We needed the flexible load balancing. And our current set of us, we have one virtual machine which is hosted in Azure. That's the editor environment. So where they edit their content. Then we have our front-end servers, as we call them. They run on web apps which autoscale. You do not need to do anything to have the flexible load balancing working. The only thing you have to do is make sure your media is accessible from all the servers. We used a custom build from a community package for doing the media to Azure blob storage. And then you need to do some config tweaks for your examine indexes so they get loaded into the instance which is fired up and not from the shared file system of the web app because you can get into blocking issues and stuff like that. But that's all documented in the load balancing documentation on ours. So... Yeah. Maybe... Maybe before I continue with this slide, I need to explain how the load balancing works. The flexible one, it uses the database. So each time an instance of your web application starts up, it will register itself in the database. And then you know which ones are active. I actually created the load balancing dashboard just to see that from your back end so you don't have to go to the database every time to find out which servers are active. And then when you publish something on your editor environment, it will write a code. And then it will send a cache instruction to a table called Umbraco cache instruction. So every time you do something in the back end, a record is inserted. And each time a request comes on the front end server, it will check... They have a file on the file system which has the ID of the last run cache instruction and it will check if it's behind. And if it's behind, it will do all the cache instruction to update your examine indexes, Umbraco content cache, and stuff like that. And that also means... What we used to do was, if we have some custom cache, for example, output cache, you clear that when you publish something in Umbraco. But that's not going to work on load balance environments because the publish action is only fired on the server where the editor pushes publish and not on all the load balance servers. So you have to hook into something called the page cache refresher. And this has a cache updated event and that will fire on all servers when it's cache. Umbraco cache is updated. So you can hook into that to update, for example, your output cache to clear that. We also have a lot of data from other sources, third party services, our own database. And you want to update that cache also if a user edits something on server X and the next request he gets on server Z. Yeah. And then you also have the same data, so you have to write your own cache refreshers. And you can do that by implementing a class with an I cache refresher interface. But then you have to write a lot of code. And Umbraco provides some base classes which you can inherit, like the cache refresher base or the JSON cache refresher base. And then you can clear your cache in the methods from that. You have a refresh all or refresh or remove method from that which you can implement. And then you can hook into a cache updated event. So when the cache refresher has updated something, then you can hook into the cache refresher event to update, for example, again your output cache. Now I'd like to explain something about the multilingual setup that we're using. We're still having a separate language tree. So we have our NL node, our EN node. And underneath that, we actually have nodes that need to be reused. So we also have a shared data folder. For the nodes in the shared data folder, we're actually using Vordo. Vordo converts a property editor into a multilingual property editor by allowing it to enter multiple values per language. Besides Vordo, we're also using nested content. Nested content can create a single or repeatable field set. For most scenarios, we actually use the single field set. I'll explain later how that will look. We're also using the models builder just to generate strongly tied models for all the published content items that we have. It's been in the course since 7.4. And we're also using it for nested content. Besides that, because we have our nodes in a shared data folder and we need the nodes to be available in all the languages, we also need to create a URL provider and a content finder. Actually, in our situation, we don't even use the node name. The node name is just a code for the editors, for them. So it's easy to find the node in the tree because we have so many content items. And then we use a Vordo text string to actually render the URL. And this is then the result that you get. We actually, today we saw a demo during the keynote about the segments and variations. That's coming in Umbraco 8. We actually needed it last year in Umbraco 7.3. And the combination of Vordo and nested content actually already made it possible. This is the end result, how it looks in new heroes. So we have our Dutch and English tab. There we have all the fields which we can switch and manage per language. And I'm quickly going to explain how we made this setup with a simple example. First we start with creating a document type with the properties that we want to be multilingual. After that, we create a nested content data type. We use the data type that we just selected because nested content uses document types to create the field set. By setting the minimum items and the maximum items to one, it will be non-repeatable. So this is our field set. Next, what we're going to do, when you use Vordo, you can choose the data type that you want to be multilingual. And in this case, we use the nested content that we just created. We want to make the field set multilingual. So once we have our Vordo data type, we just put that on our document type where we actually want to have the multilingual properties. And this again will result in how we also have it with new heroes. Are you still following me? Because I know it's a lot of steps to take. But eventually you get a very user-friendly UI. And it's also very easy to use. I've also written a blog about this. It's in the 24 days. So if you want to have more information about this, you can also read it there. Now that we have our data in Embraco, we still want to render it on the front end, of course. And this example shows how we can render the multilingual properties. So we have our nested content field set. So our nested content can return an IPublish content. Because we're using Vordo, Vordo will return the object in the correct language. It checks the current culture and based on the culture, returns the object that you want. In this case, our nested content field set will return the IPublish content. So what we have here is an IPublish content that is multilingual. And we already have the properties here in the correct language. So we only need to use getVordoValue once and then we just render the properties. And we have our multilingual fields, our one-to-one. We can actually make the rendering even a bit better by using the models builder. So for our nested content, we have created the document type. The models builder also generated a model for the document type. So what we can do is we can get our IPublish content, pass that to the models builder, and then we have our object strongly typed. So as you can see, then we can do model.content as newsItem, newsItem.news, and then we can just call our properties. So these properties are also multilingual. So it's very readable code. Easy to use. Next, this is a quick demo of a URL provider. It's not the URL provider that we're using at New Heroes because that was way too much to show here. But this actually, we have a news item here. The URL is slash news slash item. We have a date picker on the news item, and we actually want to have the date as part of the URL. So what we do, we get the IPublish content. We get the date property. And when we have the date property, we actually add that to the URL. So by doing this, with a URL provider, we have the power to change the URLs of nodes. So for a 1.1 multilingual, we have one node, and we just give it different URLs per language. Once we have the URL provider, we also... You'll get a 404 if you visit that URL because the URL's changed and RACO doesn't know what to return. We need a content finder for that to hook it all up. So in the content finder, this is the content finder to match the news example which I just showed. We're just going to get the URL, split it into segments. In this case, get all the news items that we have. Here in this example, it says rootnodes.descend or self. That is very heavy to do. You shouldn't do it in a real-life website. You should write some different code to at least get the nodes that you need to match. And I actually have an example on how to do that later on. So by combining the URL provider and the content finder, we have our nodes. It's multilingual. It has different URLs. And each node that you visit has a different URL. It will return the IPublish content in the correct language. Maybe one small remark. If you are using URL providers, be careful because you can run into performance problems really fast. We found out the hard way because we have a lot of different document types which are managed multilingual. So they could have a different URL depending on the language. We created URL providers for each document type. It was nice. Everything is small. You see for which document type which URL provider was executed. But what Umbraco does when you render a page on the front end and you call .URL to get the URL, it will go to all your content URL providers to find the correct URL. And sometimes when it's just normal Umbraco content that doesn't have URL rewriting using URL providers, it just goes to all of them. And at the moment, it took us like 40 milliseconds to get the URL of an item. And when you're rendering a navigation, you have .URL a lot. So we refactored a lot and put some good caching on that because our page was rendering in six or seven seconds. And when we refactored it, we were down below one second. So you have to be careful for performance when you use URL providers and content finders. Yeah. Then I want to tell you something about how we did our database access and also our maintenance. First, we wanted to use ORM for all our custom data tables because we need to store a lot of user data of its progress, of the exercises and stuff that it does. So we needed to select one. And we had a discussion with the development team. Some of them said Entity Framework. The two of us said no, because we want to keep external dependencies down. I was the developer. I put my foot down. I said, So maybe I can show the next slide. When you use , are there people already using ? Okay. So this will look familiar to those people. For the others, what you do is you make a class that represents your database, and you will attribute the class and the properties with like the table name, which column is which. The column is the primary key. Maybe the allowed length of the column, if it's nullable or not. So it represents your database. It's actually just a model that represents your database table. And then you can do CRUD operations using the Pitapoco API. It's all in the Umbraco Core Persistence namespace. What you can do then is create an instance of the model we just saw from the class. Then you can say to the database, insert, and you just pass in the model, and it will actually do all the, generate all the SQL statements and insert it and execute them. Same way for deleting and updating. And one thing which is really nice is that you can do strongly typed queries. So where you can just say where from, where you pass in the model so it knows from which table. And you can then do a where statement just with a lambda expression, and it will also generate an SQL statement for you. The nicest thing of Pitapoco is you can also easily override this stuff with your own database queries. So you can pass in your own database queries to execute and just have it fill your custom models. But then, we also wanted to do the maintenance of our database, like adding new tables, adding columns, creating indexes from code, because we had TeamCity set up for automatic deployments. In Umbraco, we used using already to do that. It's part of our build process. So we needed to find a solution for that. And our first attempt was just to use Pitapoco. Maybe the people who use Pitapoco already have seen this. You have a database scheme helper class in Umbraco. Then you can check if a table exists, and then you can create it or drop it. But you cannot change your table once it has been created. You have to drop it and recreate it, which makes you lose all data. So then again, the people from Entity Framework were saying, hey, Entity Framework can do that. So yeah, but hold on a minute. Umbraco does also database updates when they do an upgrade. So just we are going to have a look how they are doing. And since Umbraco 7.3, you can use the migrations. It's open to the general public, let's say. Before, you couldn't use it for your own application. It was just for Umbraco itself, but now you can use it yourself. And they actually have been using it since version 6. So they are doing their database migrations from code. And what it does is you create a base class. I mean, you can go to the next slide. A class which inherits from a base class called migration base. And then you have to also add an attribute called migration where you give the version number. And then you can have a sort order because you can have multiple files in the same version. So you can split up your migrations in multiple files. And then you can pass in an application name. So those, I'm doing a migration for this application. And what it does when it executes the migration, it checks in the database for that application what is the latest version number that I have there. It's called, the table is called Umbraco migration, I think. And then if you're behind, you can execute migrations. And on the migration base, you have two classes, methods you can implement. One is up, one is down. And it's actually where you do your upgrade and your downgrade instructions. I haven't tried the downgrade yet because we didn't have any need for it. So we only used the up method. And then when you are creating a migration, you want to create your database, of course. Then you can do, like, create a table. And it's just also strongly typed. It's just a fluent API where you can do a create table with column, primary key. But you also can change columns. For example, you want to add a column somewhere later in your development process. Or you can execute SQL statements, but you also can create foreign keys, indexes. It's even possible to execute just C sharp code, but you have to try it on your own risk. I haven't tried it yet. And if you are interested in the migrations, it's also in the core persistence namespace of the Umbraco source. So you can find it. I think the namespace is called migration, so you find everything there. Then the only thing what you need to do is execute your migrations when your application starts. And there's this piece of code. What it does is gets all the migrations from the database. It checks which one is the latest version that is installed. Then we check if it matches our target version or not. And if it doesn't match, we upgrade our database. And it will execute all the migrations in your target version. So this way it was very easy for us to maintain our database from code and have it as a part of our automatic build process. This is the second slide of the code because I couldn't put it in one slide. And there was one thing. If you're using Umbraco lower than 7.4.2, you need to have an extra cache for an HTTP exception. Umbraco, after all migrations, has done, it fires an after migration handler. There's a class that you also can implement, so you can do some code after all migrations have been fired. And it did the normal migrations what Umbraco does, like update client dependency framework, clear cookies, when it does an Umbraco upgrade, also for our own migration, and we got an HTTP exception. But I reported a bug for that, and it's fixed in 7.4.2, and it's even more flexible now. You have migrations for your own migration handlers which fire after the migrations have been executed for your own application. So it's even become a little bit better. So the migrations are pretty cool actually to use. We're probably one of the first ones that have been using them because at one point we had a custom migration. It was called 7.3.4. And no matter what we did, the migration didn't run. And usually if it doesn't run, there's something wrong with your SQL statement, or with your code, so we just kept testing, and we couldn't find out any issues with the migration. And then we discovered that Umbraco had already run migration 7.3.4 because we were already on that version. So Umbraco did not check if it was our version or their version that had to run. So it was very coincidence that we actually had a version with the same name. And it can drive you pretty crazy by having such a small file. And luckily they've created a pull request for it. It was fixed in 7.3.7, so no one will need to run in that issue anymore. Now I'm going to continue something with the performance. These are two using statements that you can use. By using these using statements, all the codes that you put in this will be locked to the logging. You can have the debug info, normal info, debug line, sorry, and the info line. And with this, it's very easy to see how long your code executes. And it's actually not just locked, it's also visible in the mini-profiler. Maybe if you look at your Umbraco log files, and then when Umbraco starts up, you will see actually the code outputted by this, because it says Umbraco is doing something, started doing this, and then it says the line after that says completed, and it took so many milliseconds. So that's actually what this code does, just puts information into your log. Yeah. And you can also see the result here, just add the umbraco debug, show traces through query string, and then you get your mini-profiler, and here you can see how long all of your parts take. Also the using statements that you added yourself, you can see how many SQL statements have been executed, so it's very easy to find performance bottlenecks like this. One of them was this actually. While looking at the SQL statements that were executed, we saw that for each dictionary item that we were using, a SQL statement was being done. This only happens after a save and publish, but if a user does a save and publish on the website, they don't change the dictionary item, so we didn't want this kind of thing. We didn't want this cache to be cleared, so we built our own cache layer around it, and that was a huge performance boost afterwards. So mini-profiler is actually also good for finding things that are wrong with umbraco itself. We're also using route hijacking. In this case, we have all our index methods. We use the donut output caching. We have a special version. It's the umbraco donut output cache. It actually does a few extra things. It doesn't cache when you're in preview mode, so that's good. And what we're using here as the attributes of donut output cache is the long page cache, so that's just cache this entire part for a day. And we're also adding an order here because if you have custom attributes, for example, an attribute that checks if you have a, you have the right credentials, then we don't want that to be cached. And by adding an order here to 100, it means that all the custom attributes that we put before the donut cache are always executed. Here we see, for example, the HTML that we have. We use a lot of HTML actions. We do that so the code is better separated, easier to reuse in other places. The last HTML action actually excludes it from the parent cache. So with the donut cache, you cache your entire page. And here we say again, this last part, we don't want to cache it for the entire day. We only want to cache it for an hour. So we exclude it from the actual cache. And then in the child action, we can actually use a different cache profile like the short page cache. Now that we're talking about the parent cache, this is a bit about performance tweaks. And this is one that we're using for Xamarin. When I was showing the content finder, it was actually using root notes, to search through the entire tree to get all the news items. There's actually a better way to do that. And this is the example we're using with Xamarin. So what we do here is we ask Xamarin, give us back all the document types that are of, or give us back all the documents types that are of, all the IDs that are specific document type. So then we only get the IDs, and then the IDs that we need, we convert them to, with the Umbraco helper, to an iPublish content. So instead of going through the entire tree, we only get what we want, and we return that as an iPublish content. And since we were using this in our content finders, the performance also went up a lot. That was the Xamarin version. Now we are on Azure, and sometimes we have a little corruption issues with Xamarin, and in that case, we added a fallback. If we don't get any results from Xamarin, for the specific document types that we have, we know that there's always a note available. If it's not, something's wrong with Xamarin. So in that case, just query the database to also get the same note IDs, and also use those note IDs again to get the correct iPublish content that we need. So, last month, our customer came to us. We wanted to have the site, it's now only in Dutch, live by beginning of November this year, totally translated in English. And we have another company that's going to do the translations, and they want XML. They have an API where you can send XML to, and then they will translate it and send the XML back. I was thinking, oh, Umbraco has a translation section where you can push a button and it generates XML. Nope. Didn't work. Empty file. So we built something new, and what we have done is we define the XML structure on property level, on property editor level, so for each property, like where you use a text box, doesn't matter where, which content type it is, it will always generate the same XML. And we have done, how we solve this, I'm going to explain now. And there's something called the Plugin Manager and Object Resolvers in Umbraco. And what we did was, we have a base class, a property translator base, which has a method you need to implement, which says for which property editor it is, and it has an export and an import method. It's a little bit like the core property value converters that you have for your published properties. Does anybody know about them? About property value converters? One person? Two? Three? Okay. I should have mentioned them too. So what the property value converter does is, for example, you have a content picker, and the content picker stores the node of your selected, the ID of the node that you selected. But if you use, for example, the core value property converters created by Jeven, it will return you the IPublished content object instead of the ID, so you do not need to query the content again to find the correct content. And we were looking at how Umbraco does that. So first thing is, yeah, the plugin manager. And plugin manager is, whenever you need to find types in assemblies, you can use a plugin manager. An old version of Umbraco, it was called Type Finder, but I think it's still in the core, but it's not recommended that you use that. And it has some methods you can say, find me all types which implement a certain interface or have a certain base class, or find all types that have an attribute, or find all types from a certain base class with an attribute, for example. The migrations, you can find them also. You just say, look up all classes that inherit from migration base and have the migration attribute, and it will scan all your assemblies and will return you all types. But it's already useful. But the real power comes when you're going to use object resolvers. And this is actually from the documentation. It's documented on our, it's just hard to find. Resolver is a class that returns one or multiple objects. And you have two types, a single object resolver and a multiple. And actually there's a third one, a lazy object resolver. But I haven't used that one yet. But you probably have seen this, you probably have seen this in the core already. For example, the default renderMVC resolver.current or when you use URL providers, URL provider resolver.current. This is actually object resolvers built into Umbraco. What we did was, yeah, made an extension on the plugin manager, an extension method, which said resolver resolved types, and find me all classes that have, this base class, property translator base. And then I created a many object resolver. You need to implement a constructor. And then I just created a property set, which is property translator and returns all the objects registered with this resolver. And then the only thing what you need to do is in your application startup, in the initialized, you say to your object resolver, find all types and populate the object resolver. And then you can use it in your code like this. You say then property translator resolver.current and then the translators. And then in our case was find the first translator for example, the rich text editor property. And this, in this case, we do not need to create XML for each document type. We just have one generic XML. And if you have a new property editor added, it's just implementing one class with an import and an export method. And all the rest of the exports will work because it's very pluggable. So if you're a package developer and you want other people to extend on your package, this is also the way to go. So we've explained a lot of techniques here that we're using for New Heroes. We're almost at the end of the presentation and I'd just like to mention a few other techniques that we're also using. So we've built a lot of custom sections using Angular, the Embraer Web API, Peter Poco. We're also using some virtual nodes. It's a bit like the content finder and the URL provider but just a different way to implement it. Of course, we're using a lot of service APIs, the content service, media service, the relation service. Actually, we're also using image processor to resize our images so they never get lost. The whole project has been built with dependency injection. We're also using some custom property value converters like Dave just mentioned to get, instead of an ID, an unprovised content or another object. And the entire project has been built with atomic design and we're using a pattern lab. So these days, everyone is using Umbraco 7.4, but I remember Umbraco 4.7. It would have been impossible to build this platform on 4.7. So I'm really happy that we're now on 7.4. We have all these techniques available. So the Umbraco core functionalities are really awesome. And actually, we've also learned a lot from the 24 days blog. There are many very useful blog posts in there that we used to also use the techniques here. Any questions? Oh, yeah? I don't like to shout too much, so... Yeah, yeah, sure, of course. We have worked on a website with one multi-language. Yes. And we used the example of the website, 24 days. Okay. And we are facing the same performance issue. Do you think there is another way to translate your URLs for maybe in the future? Well, we... Maybe repeat the question so everybody knows. They also build a website using the example from Jeroen from the 24 days calendar. And they're also running into performance issues. And now the question is, if we have tips, how to solve them? So... Yeah, well, for our system, we just added a lot of our own caching. And I think in the future, of course, we already saw the Umbraco 8 demo. Then it's using new cache. And that should already solve all the performance problems. And on the current website, like I said, just try to tweak your code with the examine example. We used, for example, then... Yeah. But for now, there is no other solution to... Well, I... Except using multi-trees languages. Yeah, currently the best setup in Umbraco is to just have the separate trees and really only have the shared data if you really need it. I don't know if... I've updated the one-one multilingual example a while ago to also have multilingual URLs. I don't know if you've... We have done it maybe a month ago. Oh, okay. Yeah, then that was already in there because I also added some... Good. Yeah. Thank you. I guess it's with the latest... It's not on. It's... It work? Yeah. So, yes, we have done it a month ago, so with the latest version of your code, maybe. Yeah, then, yeah, there already was request cache and... We have tweaked it because of also URL rewriting problems, so, and other customer specifications, so... So, it's not exactly the same code, but with the basic example we had... Maybe we can discuss this after the session because I see some other people putting up their hands. Good idea. Pete has a question. No problem. Yeah, yeah. Yeah, it's... For me, it's difficult to explain. I try to do my best, but we have like a set of base classes which do... Yeah, some work for a property, export or import XML. And then when we have created a custom menu actions and send to translator, when we do that, it will get all the properties from the content item and then it looks to the resolver if we have a translator for that. And if it has a translator, it calls the export method. If we get the XML back, it will call the import method. And it's really flexible now because if you have a new property type, we only have to create a translator base class for that. And if you have... For images, we haven't done that for an immediate picker. So it only will translate the properties that we say are translatable. But it's much easier to show in code. But it's really flexible because, yeah, you make your system pluggable. It's like some kind of inversion of control. So without actually using a dependency framework like out the fog or something. I know you are allergic to that. So here's a quick example of the... Maybe I'll go with the mic. Of the export functionality which Dave just explained for the texturing array. So we just... We read all the values. Zoom! It's a split screen, so... Let's see if that... Thank you. So first we actually... Yeah, what does it do? No. There's probably a better example with the Vordo one. So we always know that Vordo will return the IPublish content. We pass in the source language. And then we get the nested content back again. And then we export that again. Probably still not very clear, but... We can also explain that afterwards. Regarding the multi-language setup. So you use the separate trees for each language. And the shared content for each language. You just pull it into the separate trees. Or how do you manage to... Well... Do we have one URL that's available in the tree set? The shared node. Yeah. Yeah, that's what we use the URL providers and the content finders for. So it still remains in the tree. But what we do, for example, is we get one of the nodes in the trees. So we have a home page, we have a blog item. And for example, we have a multi-language blog. Then first we get the blog overview. We get that part of the URL. The URL provider gets the name from the node in the shared content. Adds that to the URL. And then you have the URL that's for both languages. And then with the content finder, you can also match that again. Okay. Any more? I have a microphone. Hello. How did you handle the situation about push? About pushing indexing file and media in a load balancing environment? Dave, maybe you can... For the media, we use Azure Blob Storage. And we used a package. I don't remember if it was the one James created or the one that Derek created. We made some customizations to it so we could have a CDN URL and stuff like that. And we just used the Blob Storage. So when you upload a media item in Umbraco, it's stored in the Blob Storage and it's available to all servers. And for the examine indexes, you have some config changes you need to do. And one is you work with a shared file system in Azure. And when you just leave your examine indexes on that one, you can get into locking issues because multiple servers are trying to read and write to it. So there is some config where you can say store the examine index in a temp folder of the instance. So the examine index is actually stored on the instance that is fired up and not on the shared file system. And there's also an option where you can say store them also on the shared file system and sync them continuously. So if there's an update, they will also get updated on the shared file system. And if a new instance starts up, it will just copy that index to the temp folder. And you have to also do that, I think, for the Umbraco config file, which is written to disk, because otherwise you get locking issues as well. You find out the hard way. Any other questions? Yeah, over there. Oh, any other questions? Then I think it's time to wrap up. The next session should start soon. Thanks, everyone. If you have more questions... pirate.åt Thank you. Thank you. Thank you. Thank you.