Çarşamba, 19 Eylül 2018

BUGFIX: SharePoint REST API ID BUG

Merhabalar,

Bir müşterinin projesi için piyasadaki Content Migration Araçları vasıtasıyla eski ortam’dan(SharePoint 2007 – WSS 3.0) yeni ortama (SharePoint 2016) ortamına liste,kütüphaneler ve diğer içerikleri taşımamız gerekiyordu.

SharePoint Content DB Migration yöntemine müşteri sıcak bakmadığından mecburen işi Content Migration Araçlarıyla halletmemiz gerekecekti.

Müşterinin ihtiyacı standard bir taşıma aracının yapamayacağı türdendi.Müşteri Liste ve Kütüphanelerdeki içeriklerin “ID bilgilerinin değişmeden” taşınmasını istemekteydi.Piyasadaki tüm content migration araçlarını denediğimizde bunu sağlayamadığını,”ID” değerini otomatik olarak ayarladığını gördük.

Bu durumda geriye ya müşteriyi Content DB Migration yöntemine ikna etmek ya da custom bir migration tool yazmak kalıyordu.İlk seçeneği müşteri istemediği için custom migration tool’unu yazmaya başladım

 

 

CUSTOM MIGRATION TOOL
Bu toolu C# ile Console Application olarak geliştirdim.Tool 2007 ortamında çalışacak ve liste/kütüphane içeriklerini REST ile 2016 ortamına aktaracaktı.

 

Toolu yazdık.Denemelerimizi yaptık.Başarılı sonuçlar vermeye başladı.90.000 kayıtlık listeyi 18 saat içerisinde(eski sürüm Windows Server sunucusu olduğunu da düşünürsek) aktardığını gözlemledik.

Migration Tool’larının bu kadar büyük listeleri açamadıklarını,açsa bile 90 günde tahmini taşıma süresi ve yarı yolda appcrash olduklarını da düşündüğümüzde 18 saat’te büyük listeleri taşıyor olmak büyük bir olaydı.

ID’ler istediğimiz gibi bir ortamdan diğerine aktarılıyordu.Herhangi bir sıkıntı ile karşılaşmadık.

Ardından Test liste/kütüphanelerden gerçek içerikleri taşımaya başladık.Test ortamında olduğu gibi tüm içerikleri bir ortamdan diğerine aktarıyorduk.

Bu haliyle yazdığım tool işimizi görecekti ve piyasadaki tüm araçların yapamadığını yapıyordu: ID’leri olduğu gibi taşıyordu

Örnek kod(payload):

RestHelper.RestPost("{'__metadata': {'type': 'SP.Data.TestUsersListItem'},
'Title': '" + item.Title + "',
'ID':'" + item.ID + "',
'Rol':'" + item["Rol"] + "'
}");

 

SORUNUN ORTAYA ÇIKIŞI

Şimdi gelelim toz pembe’den gerçekliğe…

Listeler’de içerik girişleri esnasında bazı durumlarda(bazı ID’lerde) verilerin kaydedilmediği,kaydet buton’una sürekli basılması durumunda verilerin kaydedildiği gibi bir durum ile karşılaştık J

Örneğin;

ID Title       Rol

1   İbrahim  Develope r

3   Jane        Tester

5   Ali           Manager

 

Yukarıdaki gibi bir örnek listemiz olduğunu ve bu liste içeriğinin de REST API ile ID Manipülasyonu yapılarak taşındığını varsayalım

Bu listeye eğer yeni bir eleman eklemek istersek,ID olarak “2” değerini vereceğini düşünürsünüz değil mi?

O zaman yanlış düşünüyorsunuz.

Çünkü burada ilk giriş yapıldığı için “1” değerini atamaya çalışacaktır.1 ID’li değerimiz olduğu için de bize uyarı mesajı verecektir.REST ile gönderilen ID ile SharePoint’in ID atama mekanizması arasında bir iletişimsizlik durumu mevcut olmaktadır.

REST ile ID Manipülasyonu yaptığımızda SharePoint’in bir şekilde kafası karışır ve “ID göndermek benim işim,bak şimdi kafamı karıştırdın” dermişcesine veri eklemeye çalıştığımızda bize “0x…” ile başlayan uyarılar fırlatır.Bu gayet olağan bir durum gibi gözükse de REST API’de ID değerini post etmemiz engellenmediğinden ötürü ve ID değerini ne gönderirsek gönderelim kabul ettiği için bu bir bug olarak nitelendirilebilir.

ID Manipülasyonu tehlikeli bir iştir ve dikkatli olunmadığı vakit liste/kütüphanelerin fonksiyonelliğini yitirmesine sebebiyet verebilir.Mümkün olduğunca ID atama işini SharePoint’e bırakmakta fayda vardır.

 

“Peki bu durumun üstesinden nasıl gelebilirim?

Çözümü yok mudur?

Bu bir bug ise, neden hâlen bugfix yapılmadı?”

gibi soruları duyar gibiyim.

 

REST API ile ID manipülasyonu yapılabileceği gözardı edilmiş olabilir.Microsoft’un REST API payload dokümantasyonlarını incelediyseniz,hiçbirinde ID atama işlemini göremezsiniz.

Gel gelelim bu durumun üstesinden nasıl geleceğimize?

 

 

REST API ID Manipülasyonu BugFix

Bu sorunu çözmenin şu andaki tek çözümü: EventReceiver yazmaktır

Event Receiver yazarken yakalamanız gereken olay Item oluşturulmadan önceki olay yani “ItemAdding” olayıdır.

Eleman daha oluşturulmadan yani ID henüz atanmadan SQL Content DB üzerinden AllListsAux listesindeki NextAvailableId değerini listede bulunan en son elemanın ID değerini +1 artırarak update etmelisiniz.

Tabi bunu yaparken ListID değerini de belirtmek gerekmektedir.

Kodlamaya geçmeden evvel dikkat edilmesi gereken bir husus daha var.Elements.xml dosyasını açtığınızda,Receivers tagına dikkat ediniz:

<?xml version="1.0" encoding="utf-8"?>

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

<Receivers ListTemplateId="100">

<Receiver>

<Name>TestEventReceiverItemAdding</Name>

<Type>ItemAdding</Type>

<Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>

<Class>TestProject.TestEventReceiver.TestEventReceiver</Class>

<SequenceNumber>10000</SequenceNumber>

</Receiver>

</Receivers>

</Elements>

Burada ListTemplateId’yi listenin templateID değerine göre güncelleyebilir veya ListTemplateId’yi kaldırarak tüm listeler için geçerli olmasını sağlayabilirsiniz

<?xml version="1.0" encoding="utf-8"?>

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

<Receivers>

<Receiver>

<Name>TestEventReceiverItemAdding</Name>

<Type>ItemAdding</Type>

<Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>

<Class>TestProject.TestEventReceiver.TestEventReceiver</Class>

<SequenceNumber>10000</SequenceNumber>

</Receiver>

</Receivers>

</Elements>

 

EVENT RECEIVER KODLAMA



Bildiğiniz üzere, Event Receiver oluşturulduktan sonra Item Adding olayını kod sayfasında görürsünüz

public override void ItemAdding(SPItemEventProperties properties)
{
base.ItemAdding(properties);
}

Öncelikle olarak almamız gereken bilgi Liste bilgisi olacağından ItemAdding olayını aşağıdaki gibi güncelleyelim:

public override void ItemAdding(SPItemEventProperties properties)
{
base.ItemAdding(properties);
SPList list = properties.List;
}

Properties ifadesi ile ilgili elemanın bağlı bulunduğu ve üzerinde işlem yapacağımız Liste’nin bilgisine erişmekteyiz.

Bir method yazarak parametre olarak list değişkenini gönderelim

public void UpdateNextAvailable(SPList list, Guid listId)
{

}

Event Receiver’ın çalışacağı Site’a bağlantı kurup CAMLQuery yazalım

public void UpdateNextAvailable(SPList list, Guid listId)
{
SPSite site = new SPSite("SiteURL");
SPQuery query = new SPQuery();
query.RowLimit = 1;
query.Query = "<OrderBy><FieldRef Name='ID' Ascending='FALSE' /></OrderBy>";
SPListItem item = list.GetItems(query).Cast<SPListItem>().FirstOrDefault();
}

Burada yazdığımız kod’da Siteye bağlantı sağlayarak, CAMLQuery’de listenin en son elemanının ID değerini almaktayız.

Ardından “newid” diye bir sayı tipinde değişken oluşturup değerini en son elemanın değerinden 1 büyük olacak şekilde artıralım

public void UpdateNextAvailable(SPList list, Guid listId)
{
SPSite site = new SPSite("SiteURL");
SPQuery query = new SPQuery();
query.RowLimit = 1;
query.Query = "<OrderBy><FieldRef Name='ID' Ascending='FALSE' /></OrderBy>";
SPListItem item = list.GetItems(query).Cast<SPListItem>().FirstOrDefault();
int newid = item.ID + 1;
}

Bulunduğumuz WebApplication’da Content DB olup olmadığını kontrol edip,[0] ilk sıradaki SQL Content DB’ine bağlantı sağlıyoruz:

public void UpdateNextAvailable(SPList list, Guid listId)
{
SPSite site = new SPSite("SiteURL");
SPQuery query = new SPQuery();
query.RowLimit = 1;
query.Query = "<OrderBy><FieldRef Name='ID' Ascending='FALSE' /></OrderBy>";
SPListItem item = list.GetItems(query).Cast<SPListItem>().FirstOrDefault();
int newid = item.ID + 1;
if (site.WebApplication.ContentDatabases.Count > 0)
{
var DBConnString = site.WebApplication.ContentDatabases[0].DatabaseConnectionString;
}
}

Ardından SQL Bağlantısı sağlıyoruz:

public void UpdateNextAvailable(SPList list, Guid listId)
{
SPSite site = new SPSite("SiteURL");
SPQuery query = new SPQuery();
query.RowLimit = 1;
query.Query = "<OrderBy><FieldRef Name='ID' Ascending='FALSE' /></OrderBy>";
SPListItem item = list.GetItems(query).Cast<SPListItem>().FirstOrDefault();
int newid = item.ID + 1;
if (site.WebApplication.ContentDatabases.Count > 0)
{
var DBConnString =site.WebApplication.ContentDatabases[0].DatabaseConnectionString;
SqlConnection con = new SqlConnection(DBConnString);
}
}

Ve son olarak UpdateCommand oluşturarak WSS_Content DB içerisinde yer alan AllListsAux isimli tablo’da yer alan ListID’si methodumuzun listId parametresine eşit olan kaydın NextAvailableId nümerik değerini daha önceden +1 diyerek listenin en son elemanını artırdığımız değişkenimize atama işlemini yerine getiriyoruz.

public void UpdateNextAvailable(SPList list, Guid listId)
{
SPSite site = new SPSite("SiteURL");
SPQuery query = new SPQuery();
query.RowLimit = 1;
query.Query = "<OrderBy><FieldRef Name='ID' Ascending='FALSE' /></OrderBy>";
SPListItem item = list.GetItems(query).Cast<SPListItem>().FirstOrDefault();
int newid = item.ID + 1;
if (site.WebApplication.ContentDatabases.Count > 0)
{
var DBConnString =site.WebApplication.ContentDatabases[0].DatabaseConnectionString;
SqlConnection con = new SqlConnection(DBConnString);
try
{
SPSecurity.RunWithElevatedPrivileges(delegate () 
{
con.Open();
SqlCommand com = con.CreateCommand();
com.CommandText = String.Format("UPDATE [WSS_Content].dbo.AllListsAux set NextAvailableId=" + newid + " where ListID = '{0}'", listId.ToString());
com.ExecuteNonQuery();
});
}
finally
{
con.Close();
}
}
}

ItemAdding methodunu da aşağıdaki gibi güncelleyerek event receiver’ımızı tamamlayalım

public override void ItemAdding(SPItemEventProperties properties)
{
base.ItemAdding(properties);
SPList list = properties.List;
UpdateNextAvailable(properties.List, list.ID);
}

 

Update işleminde dikkat edilmesi gereken AllListsAux isimli tabloda bulunan ilgili listenin ID’si verilerek NextAvailableId sütununu daha önceden +1 olarak artırdığımız değeri atamaktayız.

Böylelikle Event Receiver çalıştığında(event receiver’ı bir feature’a bağladığınızı ve aktive ettiğinizi varsayayıyorum) veri ekleme işi yapan kullanıcı hata almayacak ve ID’ler düzgün bir şekilde atanacaktır.

 

KODUN TAMAMI

TestEventReceiver.cs:

using System;
using Microsoft.SharePoint;
using System.Linq;
using System.Data.SqlClient;

namespace TestProject.TestEventReceiver
{
public class TestEventReceiver : SPItemEventReceiver
{
public override void ItemAdding(SPItemEventProperties properties)
{
base.ItemAdding(properties);
SPList list = properties.List;
UpdateNextAvailable(properties.List, list.ID);
}

public void UpdateNextAvailable(SPList list, Guid listId)
{
SPSite site = new SPSite("SiteURL");
SPQuery query = new SPQuery();
query.RowLimit = 1;
query.Query = "<OrderBy><FieldRef Name='ID' Ascending='FALSE' /></OrderBy>";
SPListItem item = list.GetItems(query).Cast<SPListItem>().FirstOrDefault();
int newid = item.ID + 1;

if (site.WebApplication.ContentDatabases.Count > 0)
{
string DBConnString = site.WebApplication.ContentDatabases[0].DatabaseConnectionString;
SqlConnection con = new SqlConnection(DBConnString);

try
{
SPSecurity.RunWithElevatedPrivileges(delegate () 
{
con.Open();
SqlCommand com = con.CreateCommand();
com.CommandText = String.Format("UPDATE [WSS_Content].dbo.AllListsAux set NextAvailableId=" + newid + " where ListID = '{0}'", listId.ToString());
com.ExecuteNonQuery();
});
}
finally
{
con.Close();
}
}
}
}
}

Elements.xml:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

<Receivers>
<Receiver>
<Name>TestEventReceiverItemAdding</Name>
<Type>ItemAdding</Type>
<Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
<Class>TestProject.TestEventReceiver.TestEventReceiver</Class>
<SequenceNumber>10000</SequenceNumber>
</Receiver>
</Receivers>
</Elements>

 

 

SONUÇ

 

ID(Kimlik) Manipülasyonu önerilen bir yöntem değildir.

Her ne kadar Event Receiver yöntemiyle SQL Update işlemiyle bu sorunu çözmüş olsakta, SharePoint’in kendi ataması gereken ID’lerin hiçbir suretle değiştirilmemesi gerekmektedir.

 

Eğer Liste ve Kütüphaneler’de yer alan ID’ler hiçbir suretle değişmeyecek,taşınacak yeni sistemde de birebir kullanılması sağlanacak ise Content DB Migration yöntemi kullanılarak Step by Step Migration(SP2007>SP2010>SP2013>SP2016) yapılmalıdır.

 

Eğer ki migration yapılamaz veya müşteri tarafında migration yapılması istenmezse,bu yöntemi Content DB Migration yapılamayacak durumlarda kullanabilirsiniz.

CTO @ Araf Global - C# Corner MVP(2010'dan beri) - C# Corner ve UnifyTurkiye Yazarı

4 Comments

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

This site uses Akismet to reduce spam. Learn how your comment data is processed.