One of the most challenging problems that the Add-on system should solve is handling a lot of dependencies between different products.
Site owners want to be sure that installed add-ons are compatible with the current environment. Add-on authors (developers, partners and EPiServer) want to release new product versions and make the upgrade easy for customers.
This blog post describes how the EPiServer Add-on system handles dependencies and prerequisites and provides general rules and recommendations to consider when defining dependencies and versions for add-ons.
Problem: Dependency hell
The following quote is a great description of the problem:
In systems with many dependencies, releasing new package versions can quickly become a nightmare. If the dependency specifications are too tight, you are in danger of version lock (the inability to upgrade a package without having to release new versions of every dependent package). If dependencies are specified too loosely, you will inevitably be bitten by version promiscuity (assuming compatibility with more future versions than is reasonable). Dependency hell is where you are when version lock and/or version promiscuity prevent you from easily and safely moving your project forward.
Solution: Semantic versioning
Semantic versioning is a set of rules to follow when defining version numbers for your component (public API, package, plugin, module, add-on, etc). It was suggested by Tom Preston-Werner. Please have a look at official SemVer website for details.
The general idea is that a version has three parts, Major.Minor.Patch, and each part indicates backward compatibility:
- Major: breaking changes. Increasing Major part means that this version has breaking changes and it is not backwards compatible.
- Minor: new features. Increasing Minor part means that this versions contains new functionality but it is still backwards compatible.
- Patch: bug fixes. Increasing this Patch part means that this version contains bug fixes and is backwards compatible.
A suffix appended to the Patch part and separated by dash symbol indicates prerelease version. Prerelease versions are sorted alphabetically and are always lower than release version with the same Major.Minor.Patch numbers.
Examples:
Module 2.0.0 introduces breaking changes and is not compatible with Module 1.7.0.
Module 1.7.0 is backwards compatible with Module 1.1.3, meaning that other component which depends on Module 1.1.3 should work fine with newer version 1.7.0.
There is no functional differences between Module 1.1.3 and Module 1.1.0 and these components are compatible, however 1.1.3 fixes some bugs.
NuGet supports package versioning according to the Semantic Versioning specification. In general the Add-on system follows NuGet guidelines and patterns.
Defining the add-on version
Recommendations when defining the add-on version
DO:
- Use semantic versioning conventions when defining the version. Make sure that the Major part is increased if the new version introduces breaking changes. Increase the Minor part if the new add-on version brings new functionality but possible dependents should still work fine with it. Increase only the Patch part when the new version contains minor improvements and bug fixes.
- Use a combination of
AssemblyInformationalVersionAttribute
andAssemblyVersionAttribute
when creating an add-on package from a project. - Increase the Major version number when your add-on is rebuilt and it introduces breaking changes, even if it does not provide any new functionality or bug fixes.
DO NOT:
- Avoid increasing the Major version number just because it sounds great or because sales guys ask you.
- Do not increase the Major version number depending on your dependency versions. New (Major) version of your add-on should reflect (breaking) changes in your add-on.
The add-on version can be defined in package / metadata / version node in .nuspec file:
<?xml version="1.0" encoding="utf-8"?> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata> <id>Alloy.Sample</id> <version>1.2.3</version> <authors>Alloy</authors> <description>Sample add-on.</description> </metadata> </package>
You can use special placeholders in .nuspec file if package is created from a project:
<?xml version="1.0" encoding="utf-8"?> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata> <id>$id$</id> <version>$version$</version> <authors>$author$</authors> <description>$description$</description> </metadata> </package>
In this case add-on version should be set using AssemblyInformationalVersionAttribute
or AssemblyVersionAttribute
, usually in AssemblyInfo.cs file:
[assembly: AssemblyVersion("1.2.3.0")] [assembly: AssemblyInformationalVersion("1.2.3")]
Using AssemblyInformationalVersionAttribute
allows to set semantic version in form of Major.Minor.Patch, in example above “1.2.3”. These 3 numbers will be set in the .nuspec file and used in the package file name: Alloy.Sample.1.2.3.nupkg.
The AssemblyVersionAttribute
value is used as an add-on version if AssemblyInformationalVersionAttribute
is not found. Please note that in this case you will get 4 numbers in the version and file name (Alloy.Sample.1.2.3.0.nupkg), even if you specify 3 numbers in the attribute value. I would recommend using 0 as the fourth number in the AssemblyVersionAttribute
value as in example above since it does not fit the SemVer pattern. You can use AssemblyFileVersionAttribute
if you distinguish different revisions as file versions.
Defining prerelease add-on version
Prerelease add-on versions can be defined directly in the .nuspec file or using the AssemblyInformationalVersionAttribute
when the package is created from a project:
[assembly: AssemblyInformationalVersion("1.2.3-beta")]
Enabling prerelease versions in Add-On Store
By default prerelease add-on versions cannot be installed and they are not visible in Add-On Store. Set the allowPrereleaseVersions
attribute of episerver.packaging
element to "true"
to enable installing prerelease package versions on the site:
<configuration> <!-- ... ommited configuration ...--> <episerver.packaging allowPrereleaseVersions="true" ... > <packageRepositories> ... </packageRepositories> </episerver.packaging> </configuration>
Defining the add-on dependencies
An add-on has a dependency when it relies on some other module or component that needs to be deployed or installed on the website.
Photo: http://www.caryballetconservatory.com
Before installing any add-on the system tries to find all dependencies and dependents and check if all components are compatible.
Given that component functionality usually differs from version to version and that new major versions usually contain breaking changes, information about dependency usually contains the component identifier and the lowest compatible version number or a version range.
Add-on dependencies should be defined in the .nuspec file as described in the NuGet documentation. The NuGet Versioning article describes the general idea of specifying dependency version ranges.
Try to minimize the number of dependencies and avoid dependencies on third-party components.
The main guideline when defining add-on dependencies is to always set the lowest compatible dependency version. In addition you may want to define version range to set the maximum compatible version if your add-on functionality likely won’t be compatible with next major version of the dependency.
Dependencies on other add-ons
An add-on may rely on another add-on. When this add-on is being installed to the website, its dependency is installed automatically if possible.
For example: add-on A depends on add-on B. Add-on B relies on add-on C. When the user installs add-on A, the system will also install add-ons B and C.
Use the package ID of dependency add-on as the dependency identifier and specify the lowest compatible version or version range.
Example: you have developed a component to extend the Edit UI in and work with EPiServer Commerce data. The component uses some new features released in Edit UI 2.0 and is not compatible with the first version. The Edit UI is also an add-on and its package ID is “CMS” (I agree, not the ideal name for this package. We use this ID for backwards compatibility because in EPiServer 6 we had a protected Shell module “CMS”).
<?xml version="1.0"?> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata> ... <dependencies> <dependency id="CMS" version="2.0" /> </dependencies> </metadata> </package>
Prerequisites
Prerequisites are used to define required products and ensure platform compatibility.
A prerequisite is a dependency that is not available as an add-on and cannot be installed by the Add-on system. Prerequisites should be deployed on the website before installing the dependent add-on.
All assemblies loaded in the application domain are listed as virtual packages. It allows you to define prerequisites in the same way as dependencies on other add-ons. You just add a package dependency in .nuspec file and use the assembly name (without extension) as the dependency identifier and specify the assembly version or version range.
Let’s define EPiServer 7 Commerce as a prerequisite for the sample add-on described in the previous section. As the dependency identifier use the name of the “main” prerequisite product assembly. EPiServer.Business.Commerce assembly should be a good choice in this case. Set the lowest compatible Commerce version, for example 7.0. Use a version range to define the highest compatible version of EPiServer Commerce.
<?xml version="1.0"?> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata> ... <dependencies> <dependency id="CMS" version="2.0" /> <dependency id="EPiServer.Business.Commerce" version="[7.0,8.0)" /> </dependencies> </metadata> </package>
In this example version range defines all EPiServer Commerce versions 7.x.x as compatible but excludes the next major release 8.0.0.
Dependencies on third-party components
Implicit dependencies
An implicit dependency is a server-side or client-side component that is used by an add-on but which is not defined as a dependency in the add-on .nuspec file explicitly. You create implicit dependencies if you include third-party assemblies or JavaScript frameworks in your add-on package. You may face different kinds of problems if the same components are included in other add-ons. It is impossible to control and make sure that add-ons and components are compatible with each other and with the website environment.
There are several ways to avoid this problem:
- Get rid of the additional dependencies. Consider implementing that piece of functionality in your code when it is reasonable.
- Define a prerequisite when your dependency is a product or platform that should be preinstalled/deployed to the website.
- Consider creating a separate package for each third-party component to make it possible to install these components as an add-on. In this case other developers can reference these components and use them as dependencies for their add-ons. Contact EPiServer to discuss creating dependency packages.
Never include third-party assemblies or client side frameworks in your add-on package.
Dependencies on third-party assemblies and other server-side resources
Several versions of the same assembly should not be loaded in the website AppDomain. That’s why it is especially important to make sure that dependencies on third-party assemblies and versions can be handled properly.
Google Analytics for EPiServer 7 and Social Reach add-ons use DotNetOpenAuth library for authentication. We don’t include DotNetOpenAuth assemblies to these add-ons. DotNetOpenAuth assembly is included in a separate package that can be referenced as a dependency by any other add-on.
There is no point in installing DotNetOpenAuth as a standalone add-on because this is a framework that is used by other products. You don’t see these dependency add-ons in the Add-on Store, but you get DotNetOpenAuth installed as soon as you install Google Analytics or Social Reach add-ons on your website.
Dependencies on third-party JavaScript frameworks and other client resources
The same rules should work for client side components. It would be strange if several add-ons include jQuery framework or define other JavaScript library hosted on CDN as client resources in the module manifest. Including several versions of the same framework on pages may cause conflicts or unexpected behavior and results.
Try to avoid including third-party client-side components and frameworks in your add-on. Consider implementing required functionality in your code. Otherwise consider creating dependency package for each third-party framework that can be used by other modules.
Creating dependency packages
A dependency package should contain only one third-party component. It can be created in the same way as usual add-on packages and it should follow the same rules for versioning. Dependency package can depend on other dependency packages.
The following tags are required in the .nuspec file of the dependency package: EPiServerModulePackage, EPiServerDependency.
Recommendations when defining dependencies
DO:
- Always specify the lowest compatible dependency version. Consider setting the highest compatible version to avoid incompatibility with the next major version of the dependency.
- Use the add-on package ID when defining a dependency on that add-on.
- Use the corresponding assembly name when defining a prerequisite.
- Try to avoid extra dependencies when possible.
- Define any dependency on a third-party component as an add-on dependency or prerequisite.
- Create one dependency package for each third-party component.
- Contact EPiServer when you consider creating dependency add-on packages for a third-party component
DO NOT:
- Do not include third-party components (server side or client side) in your add-on package.
- Do not use the assembly name as the dependency identifier when defining a dependency on another add-on. For example, use add-on ID “CMS” instead of assembly name “EPiServer.Cms.Shell.UI” when you define a dependency on the Edit UI.
- Do not list indirect dependencies. The .nuspec file of your add-on should state only direct dependencies on add-ons and components that are used in your add-on.
- Avoid circular dependencies.
Feedback
We are trying to define best practices and our recommendations may evolve depending on our needs and experience. That’s why your feedback is (as always) highly appreciated. Let us know about your cases!
More information
Information regarding add-ons and dependencies by Linus Ekström