Overview
AoT
is shorthand for a .Net technology to produce native binaries that can run with no JIT compilation. We have a long-term goal to make SqlManagementObjects core pieces buildable as AoT DLLs.
Benefits of an AoT build
- Substantial performance improvements.
- We can export core scenarios to be callable from non-.Net applications using
[UnmanagedCallersOnly]
attribute.
External obstacles
The main external dependency that is a long way from AoT readiness is Microsoft.Data.SqlClient
. As we make progress on AoT in SMO we will open issues against that component. Hopefully in their version 6 they can break up their monolithic driver into smaller pieces (such as removing the MSAL dependency) and the core driver could be built as AoT.
Internal obstacles
Dependence on reflection
Much of that dependence is based on SFC.
For example, a URN like Server/Database[@Name='mydb']/Option is mapped at runtime to a property on the database object by examining attributes on Type
instances.
In this example, we see how Option
is mapped to the DatabaseOptions
property of the Database
type.
The property declaration of Database.DatabaseOptions
has an attribute indicating a ChildObject
relationship:
[SfcObject(SfcObjectRelationship.ChildObject, SfcObjectCardinality.One)]
public DatabaseOptions DatabaseOptions
The DatabaseOptions
type is attributed to map its URN key:
[SfcElementType("Option")]
public partial class DatabaseOptions : SqlSmoObject, Cmn.IAlterable
These attributes come into play when SMO is trying to find or create an object from the URN, the core of which is in this function in SqlSmoObject
private void InitObjectsFromEnumResultsRec(SqlSmoObject currentSmoObject,
XPathExpression levelFilter,
int filterIdx,
System.Data.IDataReader reader,
int columnIdx,
object[] parentRow,
bool forScripting,
List<string> urnList,
int startLeafIdx)
That function has a bunch of special cases, it mixes hard coded switch statements with reflection, and it uses exceptions-as-flow-control. Let's follow this code.
Type childType;
if (urnList != null)
{
// We have to special case Server here, since we normally skip over it for Object Query
// except when it is all by itself (i.e. the query is for just "Server").
int nodeCount = levelFilter.Length;
childType = GetChildType((
nodeCount > filterIdx) ? levelFilter[filterIdx].Name : currentSmoObject.GetType().Name,
currentSmoObject.GetType().Name);
}
else
{
childType = GetChildType(levelFilter[filterIdx].Name, currentSmoObject.GetType().Name);
}
Here it's looking at the current part of the URN identified by filterIdx
to see what property in currentSmoObject
is being fetched. GetChildType
relies completely on the Name
attribute of the URN node and the Name of the Type
of the current object.
The comments above GetChildType
are informative:
// TODO: FIX_IN_KATMAI: This function is messed up beyond repair. It needs to be completely rewritten
// using SfcMetadata
// this function figures out the child type looking at the urn name and the parent name
// we need this function because names from urn do not fit the object names all the time
public static Type GetChildType(string objectName, string parentName)
That function has a huge switch statement because there's no reflection-based way to map a member that's a collection to its URN name.
For example, both Database
and Server
have a Roles
member, but they are collections of different types:
Type GetChildType {
...
case "Role":
if (parentName == "Server")
{
realTypeName = "ServerRole";
}
else
{
realTypeName = "DatabaseRole";
}
break;
...
DatabaseBase.cs:
[SfcObject(SfcContainerRelationship.ObjectContainer, SfcContainerCardinality.ZeroToAny, typeof(DatabaseRole), SfcObjectFlags.Design)]
public DatabaseRoleCollection Roles
ServerBase.cs:
[SfcObject(SfcContainerRelationship.ObjectContainer, SfcContainerCardinality.ZeroToAny, typeof(ServerRole))]
public ServerRoleCollection Roles
Notice the SfcObject
attribute doesn't have any parameter to provide the URN component name, and there's no SfcElement
attribute on the declaration that could be used instead.
Going back to InitObjectsFromEnumResultsRec
, we then find a slew of special cases to initialize child objects of specific types.
if (childType.Equals(typeof(DefaultConstraint)))
{
...
else if (childType.Equals(typeof(TcpProtocol)))
{
obj = ep.Protocol.Tcp;
}
if (null != obj)
{
InitObjectsFromEnumResultsRec(obj, levelFilter,
// next filtering level
filterIdx + 1,
reader,
// no key, so we don't advance the column index
columnIdx,
parentRow,
forScripting,
urnList,
startLeafIdx);
}
}
return;
}
Those special cases are hard to maintain and lead new SMO contributors to implement more special cases when perhaps they didn't need to. It's clearly a code design problem to have the base class SqlSmoObject
breaking encapsulation to know about all its derived types.
At this point the function has either identified the type of object identified by the URN segment or it has actually populated child object and returned so the next segment can be process recursively. In the former case we are now ready to populate the child object using a more generalized mechanism. Here it assumes that the child object is a collection until proven otherwise:
AbstractCollectionBase childColl = null;
bool isNonCollection = false;
try
{
childColl = GetChildCollection(currentSmoObject, levelFilter,
filterIdx, GetServerObject().ServerVersion);
}
catch (Exception e)
{
// The old InitChildLevel caller just wants to throw here. It never handled singletons here anyhow.
if (urnList == null)
{
throw;
}
if (!(e is ArgumentException || e is InvalidCastException))
{
throw;
}
// We come here if we ask for a child collection for a level that isn't really a collection like singletons.
// Since we still want to get into AdvanceInitRec with them, we need to process them differently.
// Only do this for the Object Query case (urnList != null), not the old InitChildLevel cases.
isNonCollection = true;
}
GetChildCollection leads us to dynamic code execution.
internal static AbstractCollectionBase GetChildCollection(SqlSmoObject parent,
string childUrnSuffix, string categorystr, ServerVersion srvVer)
{
// this actually supposes that all child collection are named like this
// For some classes we have to get the gramatically correct plural
string childCollectionName = GetPluralName(childUrnSuffix, parent);
object childCollection = null;
// Permissions is an internal property, but we do not want to open the
// door to calls to internals methods so we limit the use of
// BindingFlags.NonPublic to Permissions
if (childCollectionName != "Permissions")
{
try
{
childCollection = parent.GetType().InvokeMember(childCollectionName,
BindingFlags.Default | BindingFlags.GetProperty |
BindingFlags.Instance | BindingFlags.Public,
null, parent, new object[] { }, SmoApplication.DefaultCulture);
}
catch (MissingMethodException)
{
throw new ArgumentException(ExceptionTemplates.InvalidPathChildCollectionNotFound(childUrnSuffix, parent.GetType().Name));
}
}
else
{
childCollection = parent.GetType().InvokeMember(childCollectionName,
BindingFlags.Default | BindingFlags.GetProperty |
BindingFlags.Instance | BindingFlags.NonPublic,
null, parent, new object[] { }, SmoApplication.DefaultCulture);
}
// this should always be true in SMO, a collection will never return null
// but it can be empty
Diagnostics.TraceHelper.Assert(null != childCollection, "null == childCollection");
return (AbstractCollectionBase)childCollection;
}
This code is why every SMO collection has an internal
constructor that accepts a SqlSmoObject
parameter.
Of course, if that InvokeMember
call throws, what happens? InitObjectsFromEnumResultsRec
decides that the member isn't a collection, it's a singleton and sets isNonCollection = true
.
if (isNonCollection)
{
SqlSmoObject currObj = GetChildSingleton(currentSmoObject, levelFilter,
filterIdx, GetServerObject().ServerVersion);
if (!AdvanceInitRec(currObj, levelFilter, filterIdx, reader, columnIdx,
columnOffset, parentRow, forScripting, urnList, startLeafIdx))
{
return;
}
}
...
SqlSmoObject GetChildSingleton(SqlSmoObject parent,
XPathExpression levelFilter, int filterIdx, ServerVersion srvVer) {
...
// Do we already know what parent property points to the singleton instance?
if (!s_SingletonTypeToProperty.TryGetValue(childType, out propName))
{
SfcMetadataDiscovery metadata = new SfcMetadataDiscovery(parent.GetType());
foreach (SfcMetadataRelation relation in metadata.Relations)
{
if (relation.Relationship == SfcRelationship.ChildObject ||
relation.Relationship == SfcRelationship.Object)
{
if (childType == relation.Type)
{
// Found it
propName = relation.PropertyName;
break;
}
}
}
// Cache this result for the next time we need to lookup this type.
// Propname will be null if we know it isn't possible to use this type.
lock (((ICollection)s_SingletonTypeToProperty).SyncRoot)
{
s_SingletonTypeToProperty[childType] = propName;
}
}
if (propName == null)
{
throw new ArgumentException(ExceptionTemplates.InvalidPathChildSingletonNotFound(childType.Name, parent.GetType().Name));
}
try
{
childObject = parent.GetType().InvokeMember(propName,
BindingFlags.Default | BindingFlags.GetProperty |
BindingFlags.Instance | BindingFlags.Public,
null, parent, new object[] { }, SmoApplication.DefaultCulture);
}
catch (MissingMethodException)
{
throw new ArgumentException(ExceptionTemplates.InvalidPathChildSingletonNotFound(childType.Name, parent.GetType().Name));
}
return (SqlSmoObject)childObject;
}
Notice that GetChildSingleton
knows what property the URN component name maps to because of the SfcElement
attribute on the property declaration.
Circular dependencies
There are many instances of a low level component like Sfc or Smo having a list of types in other assemblies that it invokes dynamically. One common usage is to support Policy Based Management in SQL Server. SMO is hosted in SqlClr to implement policy evaluation and facets are identified with various Sfc attributes. The domain declarations are centralized in SfcRegistration.cs
Action items
The road to AoT is a long one and it's hard to foresee all the work that needs to be done. We can identify a few ways to make progress now.
Implement SMO collections using generics
Define interfaces and attributes to replace special cases
- Add
SfcElement
attributes to collection members to eliminate special case lookups - Define interfaces to encapsulate the special cases for child object initialization and implement them on the object types that need them
Replace the runtime use of Sfc relationship attributes with a C# code generator
See https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview for information about these code generators.
Once we have properly attributed the members to map them to URN components and implemented interfaces to encapsulate special cases for initialization, we can move the logic that maps the [parent type, urn component]
tuples to an initialization method into a static class that is created by the source generator.
Remove circular dependencies
These dependencies will need further examination to derive a better design that preserves backward compatibility.