Düzenlenen Bir Kaydın, Azure Üzerindeki Bir SignalR Servisi ile Tüm Clientlar İçin Kilitlenmesi

Selamlar,

Bugün, daha önceki makalede backend tarafından incelediğimiz, bir kaydın herhangi bir client tarafından düzenlenmesi durumunda, başka biri tarafından düzenlenememesi konusunu, bu sefer de Front-End taraftından inceleyeceğiz. Yani amaç düzenlenen bir ürünün, başkası tarafından aynı anda düzenlenmesini engellemek.

Ali yazar, Veli bozar Küp suyunu çeker azar azar. ― Barış Manço

Azure SignalR Service:

Azure üzerinde signalR servisin çalıştırılması hakkında, daha önceden yazdığım şu makaleyi okuyabilirsiniz.

Öncelikle gelin Azure üzerinde signalR servisimizi oluşturalım:

1-) Aşağıda görüldüğü gibi Azure Marketplace’den signalR aranır, ve çıkan sonuçlardan SignalR Service seçilir.

2-) Aşağıda görüldüğü gibi SignalR service seçilip, create tuşuna basılır.

3-) Aşağıda görüldüğü gibi SignalR servisinin adı, grubu, bölgesi (Türkiye için en mantıklı bölge North Europe’dur), ödeme planına göre adet ve çalışma modu seçilip oluşturulur.

Tüm özellikler doldurulduktan sonra, aşağıda görüldüğü gibi yapılan seçimler toplu olarak gösterilir ve son onay için Create butonuna basılarak, ilgili LockRowHub signalR servisi Azure üzerinde oluşturulmuş olunur.

İlgili SignalR servisin oluşturulma anında, aşağıdaki gibi bir bildiri ekranı ile karşılaşılır.

SignalR service Azure üzerinde ayağa kalktıktan sonra, aşağıdaki gibi bir ekran ile karşılaşılır. Bağlanan Client Hub sayısı ve Backend’de açılan “Server Hub” sayısı, buradan kolaylıkla monitor edilebilir. Settings’den Keys alanına gelinir ise, ilgili servisin Connection String’ine erişilir.

Keys alanına gelindiğinde aşağıdaki gibi bir ekran ile karşılaşılır. İlgili SignalR servisin yolu : “lockrow.service.signalr.net“‘dir. Ayrıca Connection String gene burada tanımlanmıştır. Back-End’de .Net Core projesinde, SignalR Azure servisine’e, buradaki Connection String ile erişecektir. İstenir ise, Connection String içinde geçen, “Primary Key”, güvenlik ihlali durumunda tekrardan yaratılarak (Regenarate Primary key) connection string’in değişmesi sağlanır.

.Net Core 5.0.101 SignalR Hub Ve WebApi

Şimdi gelin, backend tarafda .Net 5.0 ilgili End Pointlerimizi ve SignalR Hub’ımızı oluşturalım: Aşağıdaki komut ile ilgili webapi projesi oluşturulur.

Product.cs: Düzenlenecek bir product sınıfı, aşağıda görüldüğü gib oluşturulur.


Resim Kaynağı: https://www.videomaker.com/wp-content/uploads/2017/12/381-Laptop-BG-secondary-7-1392×783.jpg

Controller/ProductController(1): Bu örnekde, bir DB connection yapılmayacaktır. Herbir client için random, static ProductList’den 5 farklı ürün çekilip, geriye dönülecektir. Ayrıca verilecek her bir ürünün fiyat bilgisi de, client bazlı random olarak farklılık gösterecektir. Kısaca A client’ı için PS5 fiyatı ile, B client’ı için PS5 fiyatı farklı olacaktır.

Son olarak, makalenin devamında anlatılacak olan signalR Hub sınıfa connect olan herbir client’ın, kendine özel bir connectionID’si vardır.  Herbir Client’a özel Product listesi, “ProductDB” Dictionary List’ine, unique connectionID’leri ile birlikte atılır. Bu liste, bir nevi herbir client’a ait özel küçük bir DB’yi temsil etmektedir.

Örnek Dictionary<string, List<Product>> ProductDB:

Controller/ProductController(2): Aşağıda görüldüğü gibi ProductController’a, GetProductByName(), UpdateProduct() Actionları eklenmiştir. SignalR yapıya dahil olunca, bu methodlar güncellenecektir. 

  • GetProductByName(): Bir ürünün ilgili client için, detay bilgisinin çekildiği methoddur.
    • ProductDB.TryGetValue(connectionID, out List<Product> _Products)“: Dictionary liste içinde connect olan Client’ın SignalR ConnectionID’si için, ürün listesi var ise, _Products değişkenine atanır.
    • Product product = _Products.First(pr => pr.Name == name)“: Eğer _Products listesi dolu ise, detayı çekilecek ürünün ismine göre filter işlemi yapılır ve bulunan ilk ürün geriye dönülür.
  • UpdateProduct(): Güncellenen bir ürünün, var ise tüm clientlar için güncellendiği methoddur.
    • ProductDB.ToList().ForEach(proList => proList.Value.ForEach(pro =>“: Tüm clientların ürün listesi gezilir ve ilgili ürün ismine göre aranır.
      • pro.Price = pro.Name == product.Name ? product.Price : pro.Price“: Eğer ilgili ürün bulunur ise, bir client’ın ilgili ürün için yaptığı fiyat güncellemesi tüm clientlara uygulanır. Böylece aynı ürün için random farklı fiyatlara sahip olan clientların, ilgili ürün için fiyatları eşitlenmiş olunur.

SignalR HUB: Düzenlenen bir ürünün diğer kullanıcılarda kitlenmesini, güncelleme işlemi biten ürünün son güncel bilgisinin diğer kullanıcılara gönderilmesini, bağlanan kullanıcının clientID’sinin kendisine gönderilmesini, ayrılan kullanıcının ürün listesinin, ProductDB’den çıkarılmasını sağlayan yapıdır.

Connect olan client’ın SignalR ConnectionID’si, client-side taraftaki “GetConnectionId()” function’ına gönderilmiştir. Yani Server-Side taraftan, o an client’ın açık olan browser’ındaki function, trigger edilir.

Bir kayıt düzenlenirken, vazgeçilip Temizle button’una basılır ise, diğer kullanıcılarda kitli olan ilgili kayıdın, kilidinin açılması için ClearProduct() methodu çağrılır ve ClientSide taraftaki “PushProduct()” methodu, “false” parametres ile tetiklenerek, “Güncelle” buttonunun aktif hale gelmesi ve kilidin açılması sağlanır.

Bir client sayfayı yenilediği ya da kapattığı zaman SignalR Hub sınıfında Disconnect olur. Bu durumda kendisine ait olan product listesinin, “ProductDB” Dictionary’den çıkarılması için, aşağıdaki method yazılmıştır.

Bilmek, ileriyi görmek; ileriyi görmek, güçlü olmaktır. ―Aristoteles

Şimdi gelin servis tarafından signalR Hub sınıfının nasıl çağrılabileceğine bakalım?

signalR Hub Dependency injection:

Controllers/IHubProductDispatcher.cs: IHubContext‘in Productcontroller’ın Constructor’ında çağrılabilmesi için aşağıdaki interface yaratılmıştır. “PushProduct()” methodu ile ya üzerinde işlem yapılan product’ın kitlenmesi, ya da son güncel halinin diğer clientlara gönderilerek kilidin kaldırılması sağlanır.

Controller/HubProductDispatcher.cs: Aşağıda görüldüğü gibi “HubContext<LockerHub>“, dependency injection ile sisteme dahil edilmiştir.

“await this._hubContext.Clients.AllExcept(connectionID).SendAsync(“PushProduct”, product, isDisabled)” : İlgili method ile, ürünün üzerinde işlem yapan clientın haricindeki tüm clientlara ürünün o anki hali “product” parametresi ile, ürünün kitlenecek ya da serbest bırakılacağı bilgisi, “isDisabled” parametresi ile Client-Side tarafdaki “PushProduct()” function’ına asenkron olarak gönderilmektedir.


Resim Kaynağı: https://blogin.co/uploads/images/knowledge-sharing-culture-startup.jpg.pagespeed.ce.1ifZTVR9tV.jpg

Startup.cs: Cross Domain, signalR, Swagger ve Routing gibi tanımlamaların yapıldığı yer burasıdır.

Aşağıda görüldüğü gibi, signalR servisinin “HubProductDispatcher” sınıfı ile Depedency Injection ile projelere katılabilmesi için Singleton, yani sadece bir kere ayağa kaldırılacak ve tüm clientlar tarafından aynı nesnenin kullanılması sağlanacak kodlar yazılmıştır.

Not: Ayrıca AddJsonProtocol ile signalR servisden client-side’a gönderilen modelin propertylerinin, farklılaşıp camelcase’e dönüştürülmesi engellenmiştir.

WebApi servis sonucu, Client-Side’dan geri dönülen modellerin propertylerinin, CamelCase’e dönüştürülüp küçük harf ile başlaması engellenmiştir. Kısacası model’in Server-side’da nasıl ise, Client-Side tafata da aynısının olması amaçlanmıştır.

Aşağıda görüldüğü gibi Cross Domain problemi çözülmüştür. Yani Angular front-end tarafdan => Server Side’a erişim, güvenlik nedeni ile yasaktır. Bunun için alttaki izinler ile, sadece “localhost:4200” portundan erişimine izin verilmiştir.

Configure methodunda kullanılacak Cors Policy, “ApiCorsPolicy” olarak tanımlanmıştır. Ayrıca routing amaçlı, browser’dan “lockerHub” keyword’ü ile gelindiğinde =>LockerHub” signalR sınıfına yönlendirilir.

startup.cs:

Angular 11 UI:

Şimdi sıra geldi ürünlerin listelendiği ve güncellendiği Fornt-End Angular projesine. Aşağıdaki komut ile angular projesi yaratılır.

src/app/models: Listelenecek ve düzenlenecek Product model aşağıdaki gibi tanımlanır.

Setup: Projede kurulması gereken paketler, aşağıdaki gibidir.

src/app/app.component.html:

Aşağıda görüldüğü gibi, Submit buttonuna basılınca “saveForm()” function’ı çağrılmaktadır. Ayrıca otomatik tanımlama kapatılmıştır.

Aşağıda görüldüğü gibi, güncellenecek Product fieldlarından, Name ve Price alanları tanımlanmıştır.

Sayfada listelenecek product kayıtları için ag-grid component kullanılmışdır. Aşağıda görüldüğü gibi [rowData] ile tanımlanaca data source kaynağı, [columnDefs] ile product modelinde gösterilecek kolonlar tanımlanmıştır.

rowSelection=”single” ile her seferinde tek bir satırın seçilmesi sağlanmıştır. [frameworkComponents] kullanım amacı custom oluşturulan componentların “ag-grid” içerisinde kullanılamsının sağlanmasıdır. (gridReady) eventinde “onGridReady()” function’ı, çağrılacaktır.

Aşağıda görüldüğü gibi footer’da, Temizle ve Kaydet buttonları tanımlanmıştır. Temizlenin (click) eventinde, “clearForm()” function’ı çağrılmıştır. Kaydet zaten submit buttonudur.

app.component.html:

src/app/app.component.ts:

Eklenen kütüphaneler aşağıdaki gibidir.

Aşağıda görüldüğü gibi :

  • “_hubConnection”: signalR sınıfa bağlanılacak bir client nesnesidir.
  • “_connectionId”: Herbir client’ın signalR Hub sınıfından aldığı, unique connectionID değeridir.
  • “signalRServiceIp”: SignalR sınıfa erişim için kullanılan url adresidir.
  • “gridApi, gridColumnApi” : Grid nesnesine ve grid kolonlarına erişim için kullanılır.
  • “rowData=[ ]” : Grid’e atanacak datanın tutulduğu dizidir.
  • “Product”: Düzenlenecek ürünün atandığı modeldir.
  • “frameworkComponents”: Makalenin devamında anlatılacak, BtnCellRenderer yani Güncelle buttonu burada tanımlanır.
  • “constructor(private service: ProductService) {“: Constructor’da ürün listesini ve detayını getirecek, düzenleyecek servis dependency injection ile sisteme dahil edilir.

  • “this._hubConnection = new HubConnectionBuilder()” : SignalR Hub classına bağlanacak connector.
  • “this._hubConnection.start().then(” : Browser üzerinden, SignalR Hub sınıfına bağlanılınca çalışan function.
  • ” this._hubConnection.on(‘GetConnectionId’, (connectionId: string) => {“: Client, SignalR Hub sınıfına bağlanınca, server side tarafta yukarıda tanımlanan “OnConnectedAsync()” methodu tetiklenir. O da bağlanan client’ın browserındaki bu “GetConnectionId()” function’ını tetikler. Ve ilgili client kendisine ait unique ConnectionID’i alır.
  • “this._hubConnection.on(‘PushProduct’, (product: Product, isDisabled: boolean) => {“: Bir client, bir ürünü düzenleyeceği zaman, server side tarafdaki, “GetProductByName()” methoduna get yapılır. Burdanda signalR Hub sınıfına ait “PushProduct()” methodu tetiklenir. O da, ürünü düzenleyen client haricindeki tüm clientların burada tanımlanan “PushProduct()” function’ını tetikleyerek, güncellenecek kayıdın Güncelle button’unu pasif hale getirir. Bu koşulda isDisabled parametresi true’dur. Eğer bu parametre false ise, kayıt güncellendi demektir. O zaman da, Güncelle buttonu tıklanılabilir hale getirilip, güncel satır bulunup eskisi ile değiştirilir.

  • “sendCancelProduct()” : Client-Side tarafda düzenlenen bir kaydın, vaz geçilip temizle buttonuna basılınması ile  çağrılan methoddur. Server-Side taraftaki signalR Hb sınıfının “ClearProduct()” methodu tetiklenmiştir. O methodda diğer clientların tamamında “PushProduct()” function’ı tetiklemekte ve “isDisabled” değişkenini false göndererek, ilgili kayıt için kitli olan “Güncelle” buttonunu aktif hale getirilmesi sağlanmaktadır.
  • “getProductByName()” : Seçilen güncellenecek ürünün, adına göre detayının çekildiği functiondır. Servisden çekilen data, Product değişkenine atanmaktadır.
  • “getProductList()” : Client, Hub Sınıfına bağlandığı zaman “GetProductList()” methodu ile tüm ürün listesini çeker ve “rowData[ ]” dizisine doldurur.

  • “saveForm()”: Form üzerinde “Kaydet” buttonuna basıldığında ya da Form submit olduğunda, çağrılan functiondır. Bu makalede sadece ürün güncelleme durumu üzerinde durulmuş, bu nedenle servis katmanında sadece “UpdateProduct()” function’ı çağrılmıştır.
  • “this.rowData = this.rowData.filter(pro => pro.Name != this.product.Name):” Ayrıca güncellenen ürünün ilgili liste içerisinde de yenilenmesi için, aşağıdaki gibi rowData önce filitrelenmiş daha sonra da güncel product, ilgili listeye eklenmiştir. Not: Yorumlanan satırlar, bir liste içerisinde değiştirilecek satırı bulup değiştirme işlemini daha performanslı yapsa da, rowData[]’ya tekrardan yeni bir değer atanmadıkça, “ag-grid” tarafından değişim anlaşılmamaktadır.
  • clearForm(): Bir ürünün güncellenmesi anında, Temizle buttonuna basıldığında çağrılan functiondır. Formun edit kısmı temizlenir ve “sendCancelProduct()” function’ı çağrılarak, diğer tüm clientlarda ilgili product’ın serbest bırakılması sağlanır. Bunun için signalR Hub sınıfının “ClearProduct()” methodu çağrılır.
  • “getSelectedRow()” :  Düzenleme amaçlı bir ürün seçildiği zaman, çağrılan functiondır. Burada, “getProductByName()” function’ı çağrılmıştır. Server side taraftaki “GetProductByName()” methodu buradan çağrılarak, seçilen ürünün detayının DB’den çekilmesi sağlanmış, ayrıca diğer clientların “PushProduct()” function’ı signalR Hub sınıfı ile tetiklenerek, ilgili kaydın kitlenmesi sağlanmıştır.

Aşağıda görüldüğü gibi ag-grid’in tüm kolonları title ve beklenen field’a göre tanımlanmıştır. Ayrıca ag-grid’e özel kolon olarak “btnCellRenderer” component’ı tanımlanmıştır. Click’lenme durumunda, hangi eventin çağrılacağı(getSelectedRow()) label’ı, stil gibi bir çok tanımlama yapılmıştır. Aslında, standart “Güncelle” buttonudur.

app.component.ts:

button-cell-renderer.component.ts: ad-grid’in bir kolonunun özelleştirilerek, içine render edilen bir component’dır. Güncelle button’u, burada tanımlanır.

  • this.id” : Güncelle buttonunun ID’si “btn_“+ gönderilen product’ın ID’si şeklinde tanımlanır. Amaç güncellenen bir ürüne ait Güncelle button’unun, seçilen ürün ID’sinden bulunarak pasif ya da aktif hale getirilmesidir.

src/app/service/productService.ts: Aşağıdaki serviste, üç method bulunmaktadır. Ürün sayfasında kullanılan, tüm servis işlemleri bu sayfa üzerinden yapılmaktadır.

GetProductList(): Ürün listesinin, client’a özel alındığı servisdir. Bu neden ile client’a ait özel üretilmiş olan signalR “connectionID“, parametre olarak gönderilmiştir. Ve dönen result ag-grid’in “[rowData]” property’sine atanmıştır.

GetProductByName(): Seçilen bir ürünün unique ismine göre detayının, client’a özel olan connectionID’si ile ona ait olan dizi gurubundan çekilmesi sağlanmıştır.

UpdateProduct(): Düzenlenen bir ürünün, tüm clientların dizisinde güncellenmesi için kullanılanılmıştır.

app.module.ts:

  • İlgili “ProductService” provider olarak projeye dahil edilir.
  • AppComponent ve ag-grid’e custom olarak eklenen Güncelle button’u, “BtnCellRenderer” componentleri declarations’da tanımlanmıştır.
  • Ayrıca imports’da, AgGridModule’ü [BtnCellRenderer] ile tanımlanmıştır.

.Net Core Bir Projeye Azure SignalR Service Entegrasyonu

Makalenin başında Azure üzerinde tanımlanan signalR Servis hizmetini, yazılan bu projeye implemente edeceğiz. Amaç, performans ve yüksek trafikde Auto scale edilerek, gelen yükün kaldırılması, trafik azalınca da daha az bir kaynak ile projenin çalışmasına devam edilmesidir. Ayrıca bakım, monitor ve disaster recovery gibi maliyetlerden de Azure signalR Services kullanılarak kurtulunmuş olunur.

1-) Öncelikle, projeye alttaki paket indirilir.

2-) Alttaki komut ile UserSecret, proje içerisine dahil edilir.

3-) Makalenin başında tanımlanan, Azure üzerinde tanımlı signalR Servisin connection string değeri, aşağıdaki yorumlu alana konarak ilgili komut çalıştırılır.

4-) Aşağıda görüldüğü gibi startup.cs altındaki ConfigureServices’de, “.AddAzureSignalR()” methodu eklenir.

Bu işlem adımlarından sonra, signalR Hub class’ı local makinada çalışsa da, clientların connect olması, tüm bağlı clientlara push notify gibi sunucuya yoğun yük getiren işlemlerin tamamı, Azure üzerinden yürütülmeye başlanacaktır.

Geldik bir makalenin daha sonuna. Bu makalede, iş hayatında bolca karşımıza çıkabilecek olan, bir kayıt üzerinde işlem yapılırken, bir başka kişinin ilgili kayıt üzerinde değişiklik yapamaması konusuna, Front-End tarafından bir bakış attık. Tabi ki burada alınan önlemlere bel bağlamamak, ve önceki makalede de anlattığım gibi aynı konun, bir de Back-End yani server side tarafta da kontorolünü sağlamak gerekmektedir.

Yeni bir makalede görüşmek üzere hepinize hoşçakalın.

Source Code: https://github.com/borakasmer/SignalRLock

Source:

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

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