Giriş
Bu yazı dizimizde sizlere Dynamics CRM’den veri çekmek için
kullanabileceğiniz yapıları anlatacağım. FetchXML ile veri çekmek bence şu anda
kullanılabilecek en pratik yöntem ama bunun haricinde .Net Language-Integrated
Query(LINQ) ile early ve late binding türler üzerinden veri çekebileceğiniz
gibi Dynamics CRM’in Query Expression mimarisini kullanarak da veri
çekebilirsiniz.
Bu üçü haricinde OData ve Filtered View’ları kullanarak da
veri çekebilirsiniz. OData(Open Data Protocol) Rest tabanlı servisler için
protokol görevi gören bir veritabanı sorgulama yapısıdır. Filtered View’lar ise
standart SQL ile SQL Server üzerinden direkt veri çekmek için
kullanabileceğimiz yapılar ama Filtered View ve OData ile geriye CRM obje
sınıflarıyla veri döndürememekteyiz. Yani SQL ile bir veri çektiğinizde DataSet
ya da DataTable gibi yapılara veriyi çekebiliriz ama FetchXML ile CRM entity
sınıfından geriye dönüş alırız bu yüzden kullanım açısından ilk yöntem daha
kullanışlıdır.
LINQ ile veriler üzerinde işlem yapabilmek için Organization
Service Context sınıfını türetip projeye eklemek gerekmektedir.
Benzerliklerini bir tablo üzerinde karşılaştırırsak;
Sorgu Biçimi
|
Özellikleri
|
FetchXML
|
QueryExpression'ın bütün özelliklerine
destek verdiği gibi matematiksel işlemler ve gruplamaya da destek verir.
Sorgular XML standartlarına göre yazılır.
|
QueryExpression
|
Sorgular bir obje modeli üzerinde icra
edilir.
|
LINQ
|
QueryExpression'ın limitleriyle
sınırlıdır.
|
Yetenekleri bakımından karşılaştırırsak;
|
Odata
|
QueryExpression
|
FetchXML
|
LINQ
|
Filtered Views
|
Create, Update, Delete desteği
|
X
|
|
|
X
|
|
Sorgu sonucu kayıtları döndürme
|
X
|
X
|
X
|
X
|
X
|
Rapor yaparken kullanılabilirlik
|
|
|
X
|
|
X
|
Sorgu sonucu birden fazla türde kayıt
döndürme
|
X
|
X
|
X
|
X
|
X
|
outer joins yapabilme
|
X
|
|
X
|
|
X
|
Çalışma zamanında doğrulama
|
|
X
|
|
X
|
|
İlişkisiz nesneleri bağlama (Union)
|
|
|
|
X
|
X
|
Matematiksel İşlemleri destekleme
|
X (Sınırlı)
|
|
X
|
X
|
X
|
FetchXML
FetchXML en güzel tarafı Dynamics CRM içerisinde Gelişmiş Ara (Advanced Find)
ile oluşturduğumuz sorguların da bu şekilde sistemde kaydedilmesi. Ayrıca bu şekilde
oluşturduğumuz sorguları da .xml olarak CRM’den alabiliyoruz böylece yazdığımız
uygulama ya da rapor içerisinde de kullanabilmekteyiz. Böylece uzun uzun
FecthXML hazırlamak yerine sistemin nimetlerinden yararlanarak sorgumuzu
hazırlayabilmekteyiz. Bunun için gelişmiş bul içerisinde Fetch XML indir
düğmesine tıklıyoruz.

CRM’de de bu kullanıcı sorguları userquery, organizasyon sorguları
ise savedquery içerisinde saklanmaktadır.
IOrganizationService.RetrieveMultiple methodu ile FetchXML
sorgulaması yapabilmekteyiz bunun için FetchXMLToQueryExpressionRequest
mesajını kullanmak gerekmektedir. Ayrıca daha önce de belirttiğim gibi
aggregations yani sum, max, min, count gibi matematiksel işlemleri de FetchXML
ile yapabilmekteyiz.
Bu makaledeki örneklerde CRM Servisini çağırmak daha önceki
Singleton Tasarım Deseni üzerinden geliştirdiğim servise bağlanma metodunu
kullanıyorum. Sözünü ettiğim makaleye buradan
ulaşabilirsiniz. Bu noktayı siz de
kendinize uygun olarak değiştirebilirsiniz.
Sorguyu Hazırlama
Bu noktada örnek bir fetchXML’i inceleyelim;
<fetch mapping='logical'>
<entity name='account'>
<all-attributes/>
</entity>
</fetch>
FetchXML mutalaka “fetch” kelimeleri arasında yer almalı.
Sonrasında ise “entity” kelimesi ile geri dönecek nesnenin türünü söylüyoruz.
Sonrasında ise hangi alanların geri döneceğini ve nasıl koşullar olacağını
belirtiyoruz. Tabii yukarıdaki örnekte bunlar yok “all-attributes” ile biz
bütün alanları geri döndür diyoruz.
Size bu sorguyu SQL ile
anlatmam gerekirse : “select * from account” şeklinde olacak. Şimdi işi biraz daha
renklendirelim;
<fetch mapping='logical'>
<entity name='contact'>
<attribute name='fullname'/>
<attribute name='createdon'/>
<filter type='and'>
<condition attribute='jobtitle' operator='eq' value='Purchasing Assistant'/>
</filter>
</entity>
</fetch>
Bu sorguda ise geriye
contact yani ilgili kişi nesnesi geri dönecek ama sadece “fullname” ve “createdon”
alanları ile. Ayrıca burada bir kriterimiz de var “jobtitle” alanı “Purchasing
Assistant” olacak kayıtları alıyoruz. Yani yine SQL ile anlatırsam : “select fullname, createdon from contact where jobtitle='Purchasing Assistant'”
Burada dikkat ettiyseniz operatör diye bir ifade yer almakta. Sorgulama
yaparken değerlerin nasıl koşullarda alınması gerektiğini burada belirtiyoruz.
Yani aşağıdaki tabloda da görebileceğiniz üzere sorgu ifadeleri kısmında "Koşul
İfadesi" kısmında yazan değerler bizim normal sql cümlesinde
kullandığımız ifadelere benzemektedir. Tek fark "=","<",">" gibi
ifadelerin yerlerine "eq","gt","lt"gibi
text bazlı ifadelerin gelmiş olmasıdır.
Koşul
|
Koşul İfadesi
|
Değer
|
equals x
|
eq
|
x
|
does not
equal x
|
ne
|
x
|
is greater
than x
|
gt
|
x
|
is greater
than or equal to x
|
ge
|
x
|
is less
than x
|
lt
|
x
|
is less
than or equal to x
|
le
|
x
|
begins
with x
|
like
|
x%
|
does not
begin with x
|
not-like
|
x%
|
ends with
x
|
like
|
%x
|
does not
end with x
|
not-like
|
%x
|
contains x
|
like
|
%x%
|
does not
contain x
|
not-like
|
%x%
|
exists
|
not-null
|
|
does not
exist
|
null
|
|
anytime
|
not-null
|
|
yesterday
|
yesterday
|
|
today
|
today
|
|
tomorrow
|
tomorrow
|
|
in next 7
days
|
next-seven-days
|
|
in last 7
days
|
last-seven-days
|
|
next week
|
next-week
|
|
last week
|
last-week
|
|
this week
|
this-week
|
|
this month
|
this-month
|
|
last month
|
last-month
|
|
next month
|
next-month
|
|
this year
|
this-year
|
|
next year
|
next-year
|
|
last year
|
last-year
|
|
on x
|
on
|
x
|
on or
after x
|
on-or-after
|
x
|
on or
before x
|
on-or-before
|
x
|
in between
|
between
|
|
not
between
|
not-between
|
|
in
|
in
|
|
not in
|
not-in
|
|
equals
user id
|
eq-userid
|
|
does not
equal user id
|
ne-userid
|
|
equals
business id
|
eq-businessid
|
|
does not
equal business id
|
ne-businessid
|
|
Sorguyu Çalıştırma
Bu sorguyu çalıştırcak metod ise
RetrieveMultiple metodudur ve servisi örneklediğimizde karşımıza çaıkmaktadır.
Bu metod makalenin başında bahsettiğim QueryExpression sınıfını da alarak işlem
yapabilmektedir. Bu metodun bir de kardeşi vardır ve hazır yeri gelmişken
bundan da bahsedeyim.
Retrieve Metodu
ID’si verilen entity nesnesinin bildirdiğiniz sütünlarını bize geri
döndürür. Kullanımı çok basit olan bu
metod geriye Entity türünden bir nesne döndürür. Bu nesne zaten bizim parametre
olarak verdiğimiz nesne adının kendisidir ve bizim belirttiğimiz sutünları
doldurarak getirir. Bu metod aslında sql cümlesi olarak bakarsak “ select
alanisimleri from (nesne)entity where
entityid = '...' ”
işlemini yerine getirmektedir.
Aşağıdaki örnekle devam edelim;
Entity slead = servis.Retrieve("lead",
new Guid("7bE545CCD3-9A3A-E011-BA8B-78E7D1623F9D"),
new ColumnSet(new string [] { "fullname", "companyname"
}));
foreach (var item in slead.Attributes)
{
Console.WriteLine(item.Key + ":" + item.Value);
}
Eğer entity’nin bütün
alanlarını geri döndürmek istiyorsak new
ColumnSet(true)
komutunu ColumnSet yerine vermemiz gerekmektedir. Yani eğer siz
sorgulayacağımız nesnenin GUID türünden idsini biliyorsanız ve join gibi
işlemlerle işiniz yoksa düz mantıkta bir sorgulama yapmak için bu metodu
kullanabilirsiniz.
RetrieveMultiple Metodu ise gerçek anlamda karışık sorguları
yapmamıza olanak tanır. Ama burada tamamen object oriented bir mimari söz
konusudur yani temelde bir sorgulama cümlesi olmadan QueryExpression ya da QueryByAttribute
sınıflarının örnekleri üzerinden sorgulama işlemi yapılmasına da olanak sağlar.
Bizim odak konumuz ise
fetchXML olduğu için konuyu fazla dağıtmadan devam edelim.
Bu fetchXML sorgusunu
çalıştırmak için ise şu şekilde bir kod yazdım;
string fetchXml = @"<fetch
mapping='logical'>
<entity
name='contact'>
<attribute name='fullname'/>
<attribute name='createdon'/>
<filter
type='and'>
<condition attribute='jobtitle' operator='eq' value='Purchasing
Assistant'/>
</filter>
</entity>
</fetch>";
EntityCollection contactlist = (EntityCollection)ServiseBaglan().RetrieveMultiple(new FetchExpression(fetchXml));
foreach (var cnt in contactlist.Entities)
{
Console.WriteLine(cnt.Attributes["fullname"].ToString() + ":" + cnt.Attributes["createdon"].ToString());
}
Console.ReadLine();
Sorgu Sonucu
İşte biraz önce
bahsettiğim RetrieveMultiple metoduna fetchXML’i veriyorum. Dönüşte ise sistem
bana EntityCollection içerisinde talep ettğim entity’i vermekte. Bu noktadan
sonra foreach ile bütün kayıtar içinde dolaşabilir ve Attributes ile alanlara
ulaşabilirim.
Bu kodu çalıştırınca da
aşağıdaki gibi sonucu almaktayım;
İleri Seviye Sorgular
Yukarıda temel bilgileri
verdikten sonra ileri seviye bilgilerle buradan devam edebiliriz.
<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
<entity name='lead'>
<attribute name='fullname' />
<attribute name='createdon' />
<filter type='and'>
<condition attribute='ownerid' operator='eq-userid' />
<condition attribute='statecode' operator='eq' value='0' />
</filter>
</entity>
</fetch>
Bu sorguda ise “lead”
türünden nesneleri geri döndürmekteyiz. “fullname” ve “createdon” alanlarını
istemekteyiz sorgu ile ama “ownerid” yani kayıtların sahibi “eq-userid” diyerek
servise kim bağlandıysa onun kayıtları olacak ve durumları da aktif olacak.
Düz mantıktaki tek bir
nesne üzerinden sorgular işte bu şekilde yapılmakta ama sistem bundan daha
fazlasına izin verebilmekte yani biz eğer istersek inner ya da outer join
yaparak başka nesneler ile de ilişki içerisindeki kayıtları da geriye
döndürebiliriz. Aşağıdaki örnek üzerinden açıklamaya çalışayım;
<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
<entity name='account'>
<attribute name='name' />
<attribute name='address1_city' />
<attribute name='telephone1' />
<order attribute='name' descending='true' />
<filter type='and'>
<condition attribute='name' operator='like' value='{0}%' />
</filter>
<link-entity name='contact' from='contactid' to='primarycontactid' alias='kisi'>
<attribute name='emailaddress1' />
<filter type='and'>
<condition attribute='firstname' operator='like' value='{0}%' />
</filter>
</link-entity>
<link-entity name='systemuser' from='systemuserid' to='createdby' visible='false' link-type='outer' alias='kullanici'>
<attribute name='firstname' />
</link-entity>
</entity>
</fetch>
Burada temelde yine bir
nesne üzerinde yani “account” nesnesi üzerinden hareket ediyor gibi gözüksek de
“link-entity” düğümleriyle işi genişletiyoruz. Örnekte görebileceğiniz üzere “contact”
ve “systemuser” nesneleri üzerinden de geriye alan döndürdüğümüz gibi bunlar
üzerinden de sorgulama yapabilmekteyiz. “link-entity” içerisinde sisteme hangi
nesne ile link yapacağımızı ve bu nesnelerin hangi alanlar üzerinde birbirleriyle
ilişki içerisinde olduklarını söylemekteyiz. Ayrıca “alias” vererek de kodun
ilerleyen kısımlarında buradan gelecek alanlar için bir tanımlayıcı da
oluşturabilmekteyiz.
Burada ben “link-type”
olarak “outer” kullandım ama siz isterseniz “inner join” yapmak için “inner” de
kullanabilirsiniz.
Bu arada belirtmeliyim
ki “order” komutu ile de belli bir alan üzerinden kayıtların “ascending” ya da “descending”
olarak sıralanmasını da sağlayabilmekteyiz.
Örnekte ben firma adı ve
kişi adı için konsoldan parametre almaktayım bu yüzden orada “{0}” ifadesini görmektesiniz.
Bu sorguyu daha raht
anlamanız için SQL cümlesine çevirecek olursam;
select name, address1_city, telephone1, kisi.emailaddress1, kullanici.firstname from account inner join contact kisi on kisi.contactid = account.primarycontactid
inner join systemuser kullanici on kullanici.systemuserid = account.createdby
where account.name like '%%' and kisi.firstname like '%%'
order by account.name desc
Böyle bir sorgu yazmamız
gerekirdi. Bu kodu çalıştıracak örnek uygulama ise şu şekilde;
string fetch = @"
<fetch
version='1.0' output-format='xml-platform' mapping='logical'
distinct='false'>
<entity
name='account'>
<attribute name='name' />
<attribute
name='address1_city' />
<attribute name='telephone1' />
<order
attribute='name' descending='true' />
<filter
type='and'>
<condition attribute='name' operator='like' value='{0}%' />
</filter>
<link-entity name='contact' from='contactid' to='primarycontactid'
alias='kisi'>
<attribute name='emailaddress1' />
<filter type='and'>
<condition
attribute='firstname' operator='like' value='{0}%' />
</filter>
</link-entity>
<link-entity name='systemuser' from='systemuserid' to='createdby'
visible='false' link-type='outer' alias='kullanici'>
<attribute name='firstname' />
</link-entity>
</entity>
</fetch>";
Console.WriteLine("bir karakter
yazın:");
fetch = string.Format(fetch, Console.ReadLine());
EntityCollection groupby1_result = ServiseBaglan().RetrieveMultiple(new FetchExpression(fetch));
foreach (var c in groupby1_result.Entities)
{
Console.WriteLine("ad:" + c["name"].ToString());
Console.WriteLine("sehir:" + c["address1_city"].ToString());
Console.WriteLine("telefon:" + c["telephone1"].ToString());
Console.WriteLine("kisi eposta:" +
((AliasedValue)c["kisi.emailaddress1"]).Value.ToString());
Console.WriteLine("kullanici:" + ((AliasedValue)c["kullanici.firstname"]).Value.ToString());
Console.WriteLine("\n");
}
Console.ReadLine();
Burada bir noktanın
üzerinde durmamız gerekmekte. Kişi ve Kullanıcı üzerindeki alanlardan veri
alabilmek için “kisi” ve “kullanici” isimli alias’ları kullanmıştık. İşte bu
alanlardan veri okuyacağımız zaman şu şekilde bir kullanıma ihtiyacımız bulunmakta;
((AliasedValue)c["kullanici.firstname"]).Value.ToString()
Yani gelen değeri önce “AliasedValue”’e
parse etmemiz sonrasında ise “Value” üzerinden değerini almalıyız.
Çıktımız ise şu şekilde.
Yani adı “a” ile başlayan bir firma ve ona adı “a” ile başlayan birinci ilgili kişi
kaydı olarak eklenmiş bir kayıt bulunmakta.

Eğer XSD dosyalarını
okumayı biliyorsanız fetchXML üzerinde nereye hangi değerlerin nasıl
gelebileceğini anlayabilirsiniz. Ayrıca bunu dosya haline getirerek Visual
Studio’ya tanıtırsanız fetchXML yazarken denetleme yaparak size hataları da
gösterecektir. Ama şu anda en güzel fetchXML oluşturma yöntemi daha önce de
belirttiğim gibi Gelişmiş Ara aracını kullanmaktır.
<?xml version="1.0" encoding="utf-8" ?>
<xs:schema id="fetch" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mstns="http://tempuri.org/fetch/unique">
<xs:annotation>
<xs:documentation>Schema name: fetch-schema</xs:documentation>
</xs:annotation>
<!--
condition
element - used for capturing entity and link-entity
"where"
clause criteria
-->
<!--
[XDR-XSD] "value" element -->
<xs:element name="value" type="xs:string"></xs:element>
<!--
[XDR-XSD] "condition" element -->
<xs:element name="condition">
<xs:complexType>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<!-- -->
<!--
The
attribute "value" is used for all operators that compare to a single
value (for example, eq).
The
element "value" is used for operators that compare to multiple values
(for example, in).
Some
operators require neither the attribute "value" or the element
"value" (for example, null).
-->
<xs:element name="value" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="uiname" type="xs:string" />
<xs:attribute name="uitype" type="xs:string" />
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:choice>
<!-- -->
<xs:attribute name="column" type="xs:string" />
<xs:attribute name="attribute" type="xs:string"></xs:attribute>
<xs:attribute name="entityname" type="xs:string"></xs:attribute>
<xs:attribute name="operator" use="required" type="operator"></xs:attribute>
<!--
The
attribute "value" is used for all operators that compare to a single
value (for example, eq).
The
element "value" is used for operators that compare to multiple values
(for example, in).
Some
operators require neither the attribute "value" or the element
"value" (for example, null).
-->
<xs:attribute name="value" type="xs:string"></xs:attribute>
<xs:attribute name="aggregate" type="AggregateType"></xs:attribute>
<xs:attribute name="alias" type="xs:string"></xs:attribute>
<xs:attribute name="uiname" />
<xs:attribute name="uitype" />
<xs:attribute name="uihidden" type="TrueFalse01Type" />
</xs:complexType>
</xs:element>
<!--
filter
element - used for constructing complex conditionals
legal on entity and link-entity
-->
<!--
[XDR-XSD] "filter" element -->
<xs:element name="filter">
<xs:complexType>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<!-- -->
<xs:element ref="condition" minOccurs="0" maxOccurs="500" />
<xs:element ref="filter" minOccurs="0" maxOccurs="unbounded" />
</xs:choice>
<!-- -->
<xs:attribute name="type" default="and">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="and" />
<xs:enumeration value="or" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="isquickfindfields" type="xs:boolean" />
</xs:complexType>
</xs:element>
<!--
attribute
elements - used for selecting attributes from the
surrounding entity / link-entity, these
values are returned as part of the fetch
-->
<!--
[XDR-XSD] "all-attributes" element
-->
<xs:element name="all-attributes">
<xs:complexType></xs:complexType>
</xs:element>
<!--
[XDR-XSD] "attribute" element -->
<xs:complexType name="FetchAttributeType">
<xs:attribute name="name" use="required" type="xs:string"></xs:attribute>
<xs:attribute name="build" type="build"></xs:attribute>
<xs:attribute name="addedby" type="xs:string" />
<xs:attribute name="alias" type="xs:string"></xs:attribute>
<xs:attribute name="aggregate" type="AggregateType"></xs:attribute>
<xs:attribute name="groupby" type="FetchBoolType"></xs:attribute>
<xs:attribute name="dategrouping" type="DateGroupingType"></xs:attribute>
<xs:attribute name="usertimezone" type="FetchBoolType"></xs:attribute>
</xs:complexType>
<!--
order
element - used to specify a sort order
-->
<!--
[XDR-XSD] "order" element -->
<xs:complexType name="FetchOrderType">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<!-- -->
</xs:choice>
<!-- -->
<xs:attribute name="attribute" type="xs:string"></xs:attribute>
<xs:attribute name="alias" type="xs:string"></xs:attribute>
<xs:attribute name="descending" default="false" type="xs:boolean"></xs:attribute>
</xs:complexType>
<!--
link-entity
element - used for joining one entity to its "parent"
-->
<!--
[XDR-XSD] "link-entity" element
-->
<xs:complexType name="FetchLinkEntityType">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<!-- -->
<xs:element ref="all-attributes" minOccurs="0" />
<xs:element name="attribute" type="FetchAttributeType" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="order" type="FetchOrderType" minOccurs="0" maxOccurs="1" />
<xs:element ref="filter" minOccurs="0" />
<xs:element name="link-entity" type="FetchLinkEntityType" />
</xs:choice>
<!-- -->
<xs:attribute name="name" use="required" type="xs:string"></xs:attribute>
<xs:attribute name="to" type="xs:string"></xs:attribute>
<xs:attribute name="from" type="xs:string"></xs:attribute>
<xs:attribute name="alias" type="xs:string"></xs:attribute>
<xs:attribute name="link-type" type="xs:string"></xs:attribute>
<xs:attribute name="visible" type="xs:boolean"></xs:attribute>
<xs:attribute name="intersect" type="xs:boolean"></xs:attribute>
</xs:complexType>
<!--
entity
element - used for specifying the root element for a fetch, only
one root entity is allowed in a given fetch,
all others
are dependent on this entity and are marked as
link-entity
-->
<!--
[XDR-XSD] "entity" element -->
<xs:complexType name="FetchEntityType">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<!-- -->
<xs:element ref="all-attributes" minOccurs="0" />
<xs:element name="attribute" type="FetchAttributeType" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="order" type="FetchOrderType" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="link-entity" type="FetchLinkEntityType" />
<xs:element ref="filter" minOccurs="0" />
</xs:choice>
<!-- -->
<xs:attribute name="name" use="required" type="xs:string"></xs:attribute>
</xs:complexType>
<!--
fetch
element - root element for the query
-->
<!-- [XDR-XSD]
"fetch" element -->
<xs:element name="fetch" type="FetchType"/>
<xs:complexType name="FetchType">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<!-- -->
<xs:element name="entity" type="FetchEntityType" />
<!--
This is for the Reports view only -->
<xs:element name="order" type="FetchOrderType" minOccurs="1" maxOccurs="1" />
</xs:choice>
<!-- -->
<xs:attribute name="version"/>
<xs:attribute name="count" type="xs:integer"/>
<xs:attribute name="page" type="xs:integer"/>
<xs:attribute name="paging-cookie" type="xs:string"/>
<xs:attribute name="utc-offset" type="IntOrEmpty"/>
<xs:attribute name="aggregate" type="xs:boolean"/>
<xs:attribute name="distinct" type="xs:boolean"/>
<xs:attribute name="top