Today I held my talk on the yearly Umbraco Conference CodeGarden, due to the pandemic this year’s conference was all digital which turned out to be really good. I would like to thank everyone involved in making this a great experience it’s almost as great as the IRL experience.
So my talk was on the subject “10 things every Umbraco-developer should know” and I’ll try to create a short summary here. If you want to go into details, go ahead and download the slides.
1. Properties
A “Document Type” in Umbraco has “Properties”, these uses “Data Types” and a “Data Type” is a “Property Editor” with optional configuration.
A “Property Editor” can be used on any number of “Data Types” and a “Data Type” can be used on any number of “Document Types”.
One can alter the behavior of the “Property Editor” using “Data Type”-configuration.
2. Property Value Converts
A Property Value Converter is a class that knows how to convert the stored value into something useful for the front end.
If we use the “Multi Node Tree Picker”, the data stored in the database would be like this:
umb://document/ee82cba3a0e740639ae13026a4f72a3d,umb://document/9c1c3a2c72b045e2a3c33c068164a018,umb://document/3cce2545e3ac44ecbf55a52cc5965db3
The “Property Value Converter” knows how to take this data and create a list of IPublishedContent-items from the cache to use on the front end.
When you build your own Property Editors, don’t forget to create a Property Value Converter.
Also, do try Callum Whyte’s package Super Value Converters.
3. Database vs Cache
When you “Save” content in Umbraco it’s is saved to the database, when you “Save and Publish” it’s stored in the database and also Published to the Cache.
You should make sure that the front end of your website only uses the Cache to present data.
Do: Use IPublishedContent, UmbracoHelper, and UmbracoContext.ContentCache
Avoid: Using IContent, ContentService, MediaService, and other services.
Custom services, repositories, and background threads. In you’re your custom code the best approach to fetch data from the cache is to inject the IUmbracoContextFactory into your service.
Example:
public class BlogService : IBlogService
{
private readonly IUmbracoContextFactory _contextFactory;
private readonly IScopeProvider _scopeProvider;
public BlogService(
IUmbracoContextFactory contextFactory,
IScopeProvider scopeProvider)
{
_contextFactory = contextFactory;
_scopeProvider = scopeProvider;
}
public List<BlogPost> GetPosts(string category)
{
using (var scope = _scopeProvider.CreateScope(autoComplete:true))
{
using (var ctx = _contextFactory.EnsureUmbracoContext())
{
var blogContainer = ctx.UmbracoContext.ContentCache.GetById(1123);
var posts = blogContainer.Children.ToList();
return BlogPostMapper.Map(posts);
}
}
}
}
More details on why what is happening here can be found in this thread on our.Umbraco.com-forum.
4. Content Versions
All content in Umbraco is versioned. Every time you Save or Publish something in Umbraco a new version is created.
On the "Info"-Content App on a Content-node you can see old versions and Rollback to them if you need to.
You should avoid storing data that changes a lot (volatile data) as content in Umbraco. Ie. if you are running an import every 30 minutes, this creates 48 versions per day, in a year this is 17 520 versions.
Solution? Use the UnVersion-package that automatically cleans old versions.
5. Modes Builder
Provides strongly-typed models for the front end of your Umbraco website.
Before Models builder, we had to get a property like this: @Model.GetPropertyValue("myProperty"), but with Models Builder we can go @Model.MyProperty to get the value in a strongly typed way.
In V8, Models Builder is configured in web.config, and we tend to configure it like this:
<add key="Umbraco.ModelsBuilder.Enable" value="true" />
<add key="Umbraco.ModelsBuilder.ModelsMode" value="LiveAppData" />
<add key="Umbraco.ModelsBuilder.ModelsNamespace" value="MySite.Web.Models.Cms" />
<add key="Umbraco.ModelsBuilder.ModelsDirectory" value="~/../MySite.Web/Models/Cms" />
<add key="Umbraco.ModelsBuilder.AcceptUnsafeModelsDirectory" value="true" />
The model-classes generated by Models Builder is just a wrapper around "IPublishedContent", you can create a new instance of a typed model and pass the IPublishedContent:
IPublishedContent content = Umbraco.Content(4211);
var blogModel = new BlogPage(conten);
var title = blogModel.PageTitle;
And since Models Builder makes the Umbraco cache return actual instances of the model classes you could just cast the IPublishedContent:
IPublishedContent content = Umbraco.Content(4211);
var blogModel = content as BlogPage;
var title = blogModel.PageTitle;
When using Composition, Models Builder will create C#-interfaces for these, and you can check if an instance implements this interface (hence is using the Composition):
IPublishedContent content = Umbraco.Content(4211);
string heroImageUrl = null;
if (content is IHero hero)
{
heroImageUrl = hero.HeroImage.Url;
}
6. Debugging
Use the Log Viewer in the Settings section to view the logs and entries created by the website.
Also, these files can be found on disc in /App_Data/Logs
Use a tool like Compact Log Viewer to watch the logs outside of the backoffice.
Also, you can write to the log from your custom code:
using Umbraco.Core.Logging;
public class MyThing : IMyThing
{
private readonly ILogger _logger;
public MyThing(ILogger logger)
{
_logger = logger;
}
public void DoSomething(string value)
{
_logger.Info<MyThing>($"My thing executed DoSomething()");
}
}
You can also use measure the performance of certain blocks in your code using the IProfilingLogger:
using Umbraco.Core.Logging;
public class MyThing : IMyThing
{
private readonly IProfilingLogger _profLog;
public MyThing(IProfilingLogger profLog)
{
_profLog = profLog;
}
public void DoSomething(string value)
{
using (_profLog.TraceDuration<MyThing>("Starting work","Done with work"))
{
Thread.Sleep(250);
}
}
}
7. Lucene / Examine
Examine is the built-in "Search Engine" in Umbraco, it used Lucene.NET to index Content, Media, and Members.
It provides fast free text search and is great to perform filtering on large data sets, ie. a product or article filter.
Some take away's:
- The Umbraco 8-cache (NuCache) is a lot faster than the V7-cache
- Prefer NuCache if you're working with less than 500 content nodes and few filtering options
- Use Examine/Lucene to filter larger data sets with many filtering options
- Do test what's best for you
A more detailed presentation around this can be found here.
8. Inversion of Control / Dependency Injection
The concept of injecting dependencies into your classes is a good practice and it becomes more important with Umbraco 9 that runs in .NET 5 where DI is a first-class citizen.
LifeTimes in Umbraco 8 (LightInject)
- Transient: A new instance every time
- Singleton: Same instance for the application lifetime
- Scope: New instance for every "Scope" in the DI-container. In Umbraco, this is for every web request.
- Request: New instance for every request to the container. This is similar to Transient. Avoid this.
9. NPoco
The "Micro ORM" used by Umbraco Core, used to read, update and delete data in the database. You can think of this as a "Lightweight and fast Entity Framework". You are free to use EF in your own code if you want to but you can also use NPoco:
public class MovieRepository : IMovieRepository
{
private readonly IScopeProvider _scopeProvider;
public MovieRepository(IScopeProvider scopeProvider)
{
_scopeProvider = scopeProvider;
}
public bool Save(Movie movie)
{
return true;
}
public List<Movie> GetByYear(int year)
{
using (var scope = _scopeProvider.CreateScope(autoComplete:true))
{
var dtos = scope.Database.Fetch<MovieDto>(
scope.SqlContext.Sql().SelectAll()
.From<MovieDto>()
.Where<MovieDto>(x=>x.Year == year));
return MovieMapper.Map(dtos);
}
}
}
The Scope's created but IScopeProvider can be nested, but: Don't forget to "Complete" your scopes.
10. AngularJS hacks for the backoffice.
You can use AngularJS's $httpProvider.interceptors to intercept requests/responses to/from the Umbraco backoffice APIs.
This is useful if you would like to alter the response coming back from the API in any way. One example is to remove the default "Consent"-field that is included when creating a new form in Umbraco Forms.
angular.module('umbraco.services').config([
'$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push(['$q','$injector', function ($q, $injector) {
return {
'response': function(response) {
// Overrides the response from the API-endpoint that created the Forms,
// The controller is hardcoded to always append the "data consent"-field as the last field in the collection
// So by running pop() on the collection.
// Does the returned content match the endpoint for GetScaffoldWithWorkflows?
if (response.config.url.indexOf('backoffice/UmbracoForms/Form/GetScaffoldWithWorkflows?template=') > -1) {
response.data.pages[0].fieldSets[0].containers[0].fields.pop();
}
return response;
}
};
}]);
}]);
You need to include this javascript in the backoffice using a package.manifest-file.