List new features in TOM library

How-to: Auto-generate a list of new features released in Analysis Services Tabular

This week, a new version (19.2.0.2) of the Tabular Object Model (TOM) was released on NuGet. In fact, this is a major version jump from the previous 18.7 release:

Those updates have been more frequent recently, and they usually coincide with new features coming to Azure Analysis Services or Power BI (Premium), and allow open-source tools like Tabular Editor to support those features.

So, those releases are generally very happy occasions and allow to become more productive with the wider Power BI ecosystem. However, it has always bothered me that the AMO/TOM libraries come with very sparse documentation, and virtually no change log or release notes. Lots of the functionality has to be discovered the hard way by trial and error.

The 19.x version number has made me particularly curious this time, and I wanted to know more. I remembered from previous use of TOM that I would occasionally receive a CompatibilityViolationException when running some code against an older on-prem Tabular server, for instance.

Taking a quick look inside reveals that certain parts of the API are annotated with an CompatibilityRequirement attribute:

The number referenced there clearly corresponds to the well-known Compatibility Level for tabular models. Remember, 1200 was the level that properly kicked off tabular modeling with the introduction of TMSL, and 1400 was another milestone with the introduction of M/PowerQuery integration into Tabular and structured data sources.

Since the tight integration between Analysis Services and Power BI, quite a few new compatibility levels have been introduced, very recently 1520 in conjunction with the enhanced PBIX metadata format.

So it didn't take very long to write a little LinqPad script to automatically extract and list all TOM API elements that happen to be annotated with that [CompatibilityRequirement] attribute.

Turns out the attribute internally distinguishes three different scopes, namely: "Box" (presumably on-prem SSAS), "Excel", and "Pbi". Since 1400 has been around for a long time now, the results are split into two tables below: All features introduced after 1400 first, then all others that are marked with 1200, 1400, or Unsupported.

How to read this? To take an example, the features around Calculation Groups were introduced with level 1470 (which is also mentioned in Kaspar's blog post here). However, the ability to define a custom sort order for calculation items via the Ordinal property was only introduced with the 1500 level, hence requires a model at that level and a server that supports it as well.

Having checked one of our production Azure Analysis Services servers, they currently report those SupportedCompatibilityLevels: 1100,1103,1200,1400,1450,1455,1460,1465,1470,1475,1480,1500,1510,1520,1530,1000000, i.e. go up as far as 1530.

Hence, it is very interesting to see one item listing "1535" below (MAttributes), and even some explicitly marked as "Preview" (AnalyticsAIMetadata).

What is going to be announced here??

In any case, I hope this helps some folks (like myself) getting a bit more clarity about the features supported by the TOM API, and how those relate to the various new compatibility levels that were introduced post-1400.

The script used here is provided at the bottom, and can easily be re-run with any future releases appearing on NuGet!

Table 1: Post-1400 Features

Table 2: 1200/1400 Features

LINQPad script

Requires NuGet package: Microsoft.AnalysisServices.retail.amd64 and namespace declaration: TOM = Microsoft.AnalysisServices.Tabular.

  • TOM-Extract-CompatibilityRequirementsAttributes.linq file available in this Gist.

var asm = typeof(TOM.Server).Assembly;
var compatAttr = asm.GetType("Microsoft.AnalysisServices.Tabular.CompatibilityRequirementAttribute");

string ReadProperty(string name, object attr) => compatAttr.GetProperty(name).GetValue(attr).ToString();
string GetMemberType(Type t) => t.IsEnum ? "Enum" : t.IsInterface ? "Interface" : "Class";
Attribute GetCustomAttributeSafe(MemberInfo member, Type t) 
{ // This is needed to avoid errors on a few specific attributes containing unsupported expressions - we're simply ignoring those
    try {
        return member.GetCustomAttribute(t);
    }
    catch (TOM.TomException) {
        return null;
    }
}

var members = asm.GetTypes()
    .Select(t => new
    {
        Type = t,
        CompatAttribute = t.GetCustomAttribute(compatAttr)
    })
    .Where(x => x.CompatAttribute != null)
    .Select(x => new 
    {
        Name = x.Type.FullName,
        MemberType = GetMemberType(x.Type),
        Box = ReadProperty("Box", x.CompatAttribute),
        Excel = ReadProperty("Excel", x.CompatAttribute),
        PBI = ReadProperty("Pbi", x.CompatAttribute)
    })
    .Union(
        asm.GetTypes()
        .SelectMany(t => t.GetMembers())
        .Select(m => new 
        {
            Member = m,
            Name = $"{m.DeclaringType.FullName}.{m.Name}",
            CompatAttribute = GetCustomAttributeSafe(m, compatAttr),
            MemberType = m.MemberType.ToString()
        })
        .Where(x => x.CompatAttribute != null)
        .Select(x => new
        {
            x.Name, 
            x.MemberType,
            Box = ReadProperty("Box", x.CompatAttribute),
            Excel = ReadProperty("Excel", x.CompatAttribute),
            PBI = ReadProperty("Pbi", x.CompatAttribute)
        })
    )
    .Where(x => /* toggle this for 1200/1400: */ !(((int.TryParse(x.Box, out var box) && box <= 1400) || x.Box == "Unsupported")
        && ((int.TryParse(x.Excel, out var excel) && excel <= 1400) || x.Excel == "Unsupported")
        && ((int.TryParse(x.PBI, out var pbi) && pbi <= 1400) || x.PBI == "Unsupported")))
    .OrderBy(x => x.Name)
    .ToArray()
    .Dump();

// Convert to markdown table for blog post:
Array.ForEach(members, x => $"| {x.Name.Substring("Microsoft.AnalysisServices.Tabular.".Length)} | {x.MemberType} | {x.Box} | {x.Excel} | {x.PBI} |".Dump());

Last updated