.Net Core SignalR Hub Sınıfı Özelleştirip Dependency Injection ile Kullanma
Selamlar,
Bu makalede signalR hub sınıfına ait bir methodu, başka bir sınıftan dependency Injection yardımı ile çağıracağız. Gelin kısaca konu ile alakalı bir senaryo oluşturalım. Ekanda bir küp şekil olsun. Klavyenin ok tuşlarına basıldığı zaman, küp şekli istenen yönde hareket etsin ve bu hareketin, tüm clientlara bildirilmesi aynı zamanda da loglanması için bir webservisine istekde bulunulsun. Bu durumda, webservisi üzerinden bir signalR Hub sınıfına bağlanılır ve hareket methodu trigger edilerek, bu sayfayı dinleyen tüm clientlardaki küplerin de aynı hareketi yapması sağlanır. Hub sınıfına bağlanan örnek kod aşağıdaki gibi olabilir:
1 2 3 4 5 6 7 8 9 10 11 |
static HubConnection connectionSignalR; public static async Task ConnectServer1() { connectionSignalR = new HubConnectionBuilder() .WithUrl("http://localhost:1923/animeHub") .Build(); await connectionSignalR.StartAsync(); } ConnectServer1().Wait(); await connectionSignalR.InvokeAsync("Move", moveType); |
İşte tam olarak yukarıdaki kodu yazmak istemiyoruz. Neden ?
Önemli Not: Öncelikle eğer bir microservices yazmıyor isek, ya da yazacağımız webservices’i, ilgili signalR Hub sınıfı ile aynı sunucu üzerinde ise, yukarıdaki yöntemden başka bir yola gitmek daha doğru olacaktır. Aynı sunucu üzerinde olan signalR Hub sınıfına bağlanmak, hem performans anlamında hem de kod kalitesi açısından pek de sağlıklı değildir. Bu durumda signalR Hub sınıfının, Dependency Injection ile ilgili webapi servisinde kullanılması, performans ve kod kalitesi anlamında çok daha doğru bir yoldur.
Öncelikle aşağıdaki komut ile bir webapi projesi oluşturulur.
1 |
dotnet new webapi -o signalRDependency |
Oluşan proje içinde, öncelikle signalR AnimeHub sınıfı aşağıdaki gibi oluşturulur: Herhangi bir client’ın connect olması durumunda, connect olan client’a connectionId’si gönderilir. ConnectionId’ye neden ihtiyacımız olduğuna, makalenin devamında değinilecektir.
1 2 3 4 5 6 |
public class AnimeHub : Hub { public override async Task OnConnectedAsync(){ await Clients.Caller.SendAsync("GetConnectionId",this.Context.ConnectionId); } } |
Tam bu sırada, signalR Hub sınıfı ile WebApi servisi arasında ara bir katmanın oluşturulması, genişletilebilir bir yapının tüm olanaklarını kullanabilmemizi bize sağlar . İlgili katmanda, signalR Hub sınıfında olması istenen tüm methodlar özelleştirilerek yazılır.
IHubAnimeDispatcher: Dependency Injection için, ilgili “HubAnimeDispatcher” class’ı, “IHubAnimeDispatcher” interface’inden türetilir. Bu interface’de, kullanılması zorunlu kılınan method “Move()” methodudur. Parametre olarak “MoveType” enumu ve “connectionId” beklemektedir. Yani block’un hareket ettirildiği yön ve 2. bir parametre olarak client’ın Hub sınıfından aldığı connectionId beklenmektedir. ConnectionId’nin alınmasında amaç, hareket’in yapıldığı client’a, “Move()” methodunun tekrardan triger edilmemesidir. Yani ilgili clientda, zaten hareket eden block’a, tekrardan hareket et komutunun çalıştırılmaması amaçlanmaktadır.
1 2 3 4 5 6 |
using System.Threading.Tasks; public interface IHubAnimeDispatcher { Task Move( MoveType moveType,string connectionId); } |
MoveType: Klavyede Keydown ile basılan yön-ok tuşlarının ascii kodları, enum olarak MoveType’da tanımlanır.
1 2 3 4 5 6 7 |
public enum MoveType { Up = 38, Down = 40, Right = 39, Left = 37 } |
HubAnimeDispatcher: İlgili sınıfda, signalR “IHubContext<AnimeHub>” tanımlanır ve Dependency Injection ile Constructor’ında atanır. Böylece ilgili HubContext’e erişilmiş olunur. Bundan sonra signalR Hub class’ında kullanılmak istenen methodlar, bu özelleştirilmiş sınıf altında tanımlanabilir. Mesela bu örnekte, ilgili bloğu hareket ettirmek için Move() methodu tanımlanmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; using signalRDependency.Controllers; public class HubAnimeDispatcher : IHubAnimeDispatcher { private readonly IHubContext<AnimeHub> _hubContext; public HubAnimeDispatcher(IHubContext<AnimeHub> hubContext) { _hubContext=hubContext; } public async Task Move(MoveType moveType,string connectionId) { await this._hubContext.Clients.All.SendAsync("MoveBlock",moveType,connectionId); } } |
İlgili HubAnimeDispatcher sınıfının, Dependency Injection ile kullanılabilmesi için Startup.cs’de aşağıdaki gibi tanımlanması gerekmektedir. Ayrıca projede signalR kullanılacağı için, AddSignalR() methodu çağrılır. Son olarak Cross Domain hatasına düşmemek için, ilgili izinlerin aşağıdaki gibi verilmesi gerekmektedir :
Startup.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public void ConfigureServices(IServiceCollection services) { services.AddSignalR(); services.AddControllers() .AddNewtonsoftJson(); services.AddSingleton<IHubAnimeDispatcher, HubAnimeDispatcher>(); } services.AddCors(options => options.AddPolicy("ApiCorsPolicy", builder => { builder.WithOrigins("http://localhost:4200") .AllowAnyMethod() .AllowAnyHeader() .Build(); })); |
Startup.cs / Configure(): Ayrıca SignalR Hub sınıfı, aşağıdaki gibi Configure methodu içinde routing amaçlı tanımlanmalıdır. Cross Domain’e düşmemek için, ilgili izinlerin verilmesi gerekmektedir. ConfigureServices’de tanımlanan Policy, burada UseCors() ile beraber kullanılır.
1 2 3 4 5 6 7 8 9 |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseSignalR(routes => { routes.MapHub<AnimeHub>("/animeHub"); }); app.UseCors("ApiCorsPolicy"); } |
Projenin çalışacağı portun özelleştirilmesi için, program.cs altında CreateHostBuilder() methoduna “webBuilder.UseUrls()” tanımlaması gerekmektedir.
Program.cs:
1 2 3 4 5 6 7 |
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); webBuilder.UseUrls("http://localhost:1923"); }); |
Log : Her yapılan hareketin Move(), yani yönlendirme işinin Sql bir DB’ye yazılarak Loglanması sağlanır.
Model/MoveLog: Öncelikle, aşağıda görüldüğü gibi MoveLog adında yeni bir model oluşturulur.
1 2 3 4 5 6 7 8 9 10 |
using System; [Serializable] public class MoveLog { public int ID { get; set; } public string ConnectionId { get; set; } public int Direction { get; set; } public DateTime CreatedDateTime { get; set; } } |
SqlDB tarafında aşağıdaki gibi bir tablo oluşturulur.
.Net Core Projesine, aşağıdaki komut ile EntityFramework kütüphanesi indirilir.
1 |
dotnet add package Microsoft.EntityFrameworkCore.SqlServer |
Dal/CustomerDataContext : DbContext aşağıdaki gibi oluşturulur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using Microsoft.EntityFrameworkCore; public class CustomerDataContext : DbContext { public CustomerDataContext(DbContextOptions<CustomerDataContext> options) : base(options) { } public DbSet<MoveLog> MoveLog { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); optionsBuilder.UseSqlServer("Server=tcp:10.211.55.9,1433;Initial Catalog=Customer;User ID=sa;Password=******;"); } } |
Startup.cs/ConfigureServices() : Yukarıda oluşturulan “CustomerDataContext”‘in Startup.cs’de, Dependency Injection ile kullanılabilmesi için aşağıdaki gibi tanımlanır.
1 |
services.AddDbContext<CustomerDataContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), opt => opt.UseRowNumberForPaging())); |
appsettings.json: DefaultConnection aşağıdaki gibi tanımlanır.
1 2 3 |
"ConnectionStrings": { "DefaultConnection": "Server=tcp:10.211.55.9,1433;Initial Catalog=Customer;User ID=sa;Password=******;" } |
AnimeController.cs : Sıra geldi, esas webapi servisinin oluşturulmasına.
- “private readonly IHubAnimeDispatcher _dispatcher” : Erişilecek Hub interface’ı, burada tanımlanmıştır
- ÖNEMLİ : “public AnimeController(IHubAnimeDispatcher dispatcher)” : Makalenin esas amacı olan, Constructor‘da “IHubAnimeDispatcher” sınıfı parametre olarak alınır. Ve “_dispatcher“‘a atanır. Böylece ilgili Hub sınıfına connect yapılmadan erişilmiş olunur. Ayrıca Loglama için “CustomerDataContext” dependency Injection ile _context nesnesine atanmıştır.
- “public ActionResult<IEnumerable<string>> Get()” : Test amaçlı webApi servisi ayaktamı diye kontrol amaçlı yazılmış bir methoddur.
- “public async Task<ActionResult> Get(MoveType moveType, string connectionId)”: ClientSide taraftan hareket ettirilen block’un hareket yönü, diğer clientlara webapi servisi çağrılıp signalR ile haber verilmektedir. Bu işlem sırasında yukarıda tanımlanan “HubAnimeDispatcher” sınıfının “Move()” methodu çağrılmıştır. Parametre olarak, hareketin yönü MoveType ve hareketin yapıldığı client’ın signalR ConnectionId’si(connectionId) kullanılmaktadır.
- Yapılan hareketin MoveLog tablosuna loglanması için, “_context.MoveLog” tablosuna, yeni bir kayıt atılır ve _context.SaveChanges() methodu ile kaydedilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
[Route("api/[controller]")] [ApiController] public class AnimeController : ControllerBase { private readonly IHubAnimeDispatcher _dispatcher; private CustomerDataContext _context; public AnimeController(IHubAnimeDispatcher dispatcher, CustomerDataContext context) { _dispatcher = dispatcher; _context = context; } [HttpGet] public ActionResult<IEnumerable<string>> Get() { return new string[] { "value1", "value2" }; } // GET api/values/5 [HttpGet("{moveType}/{connectionId}")] public async Task<ActionResult> Get(MoveType moveType, string connectionId) { await this._dispatcher.Move(moveType, connectionId); this._context.MoveLog.Add(new MoveLog() { ConnectionId = connectionId, Direction = (int)moveType, CreatedDateTime=DateTime.Now }); this._context.SaveChanges(); return Ok(); } } |
Örnek MoveLog tablosundan kayıtlar:
Web Client:
Web uygulaması için Angular 8.1.3 projesi aşağıdaki gibi oluşturulur:
1 |
ng new signalRdependencyClient |
Oluşturulan projeye, signalR kütüphanesi aşağıdaki komut ile eklenir:
1 |
npm install @aspnet/signalr –-save |
app.component.ts(1): Aşağıda görülen signalR kütüphaneleri import edilir.
1 2 |
import { HubConnection, HubConnectionBuilder, LogLevel } from '@aspnet/signalr'; import * as signalR from '@aspnet/signalr'; |
app.component.ts(2):
- Aşağıda görüldüğü gibi signalR Hub sınıfınına connect olunması durumunda console’a “Hub Connection Start” mesajı yazılmıştır.
- GetConnectionId() client side function’ında connect olan client’ı ekrana basılmıştır.
- MoveBlock() makalenin devamında yazılacaktır. Şimdilik esas amcı ekrandaki bloğun hareket ettirilmesidir. Makalenin devamında, içi doldurulacaktır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
export class AppComponent { title = 'signalR Küp Oynatmaca'; _hubConnection: HubConnection; _connectionId: string; signalRServiceIp: string = "http://localhost:1923/animeHub"; public ngOnInit(): void { this._hubConnection = new HubConnectionBuilder() .withUrl(`${this.signalRServiceIp}`, { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets }) .build(); this._hubConnection.start().then( () => console.log("Hub Connection Start")) .catch(err => console.log(err)); this._hubConnection.on('GetConnectionId', (connectionId: string) => { this._connectionId = connectionId; alert(`ConnectionID:${this._connectionId}`); console.log("ConnectionID :" + connectionId); }); this._hubConnection.on('MoveBlock', (commandId: number) => { console.log("Move Block :" + commandId); }); } } |
app.component.html: Html sayfası, yukarıda görüldüğü gibi kırmızı bir başlık ve hareket ettirilecek mor bir kareden oluşmaktadır.
1 2 3 4 5 6 7 8 9 10 |
<div style="text-align:center"> <h1> <font color="red">{{ title }}</font> </h1> <div id="animationWrapper"> <div #square id="square" style="top: 0px;"></div> </div> </div> |
app.component.scss: Html sayfa üzerindeki karenin ve hareket yapılacağı div’in style’ı aşağıdaki gibi tanımlanmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#animationWrapper { position: relative; height: 50px; } #square { position: absolute; left: 0px; top: 0px; width: 50px; height: 50px; background-color: rgba(0, 0, 220, 0.3); } |
- SQUARE_SIZE: Karenin boyutu bir değişkende saklanır.
- MOVEMENT_STEP: Karenin her bir hareketin için ilerleyeceği pixel miktarıdır.
- requestAnimationFrame : Bloğun animatif olarak hareket ettirilmesi için atanan değişkendir.
- direction : Bloğun hareket edeceği yöndür. Default 12 atanmıştır. Hiçbir yönü ifade etmediği için, en başta blok durmaktadır.
moveSqure() : Karenin hareket ettirilmesi, aşağıdaki method ile yapılmaktadır.
- left,top,right,bottom : Öncelikle karenin bulunduğu noktanın kordinatları, karenin height ve weight’i de göz önüne alınarak bulunur.
- switch (this.direction) { } : Direction ile emir verilen hareket yönü belirlenir.
- 37,38,39,40 : İstenen yöne, belli bir aralığa (MOVEMENT_STEP) göre hareket ettirilir.
- if (right < document.documentElement.clientWidth) : 39 (Sağ) için ekranın genişliğine kadar izin verilir.
- if (bottom < document.documentElement.clientHeight-80) : 40 (aşağı) için ekranın yüksekliği kadar izin verilir. Ordaki “-80”, karenin en son sayfanın tabanında durması için yapılan küçük bir rutuşdur :)
- requestAnimationFrame(): Bloğun, animatif olarak hareket etmesini sağlayan function’dır.
- @ViewChild : Html taraftaki #squre olarak tanımlı “div”‘i ifade etmektedir. Böylece component tarafında ilgili kareye erişilip, pozisyonu değiştirilebilecektir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
SQUARE_SIZE: string = "50"; MOVEMENT_STEP: string = "3"; requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame; direction: number = 12; // stop @ViewChild('square', { static: false }) square; public moveSquare(): void { var left = parseInt(this.square.nativeElement.style.left, 10); var top = parseInt(this.square.nativeElement.style.top, 10); var right = left + parseInt(this.SQUARE_SIZE); var bottom = top + parseInt(this.SQUARE_SIZE); console.log("Direction :" + this.direction); switch (this.direction) { case 37: // left if (left > 0) { this.square.nativeElement.style.left = left - parseInt(this.MOVEMENT_STEP) + 'px'; this.requestAnimationFrame(this.moveSquare.bind(this)); } break; case 38: // up if (top > 0) { this.square.nativeElement.style.top = top - parseInt(this.MOVEMENT_STEP) + 'px'; this.requestAnimationFrame(this.moveSquare.bind(this)); } break; case 39: //right if (right < document.documentElement.clientWidth) { this.square.nativeElement.style.left = left + parseInt(this.MOVEMENT_STEP) + 'px'; this.requestAnimationFrame(this.moveSquare.bind(this)); } break; case 40: // down if (bottom < document.documentElement.clientHeight-80) { this.square.nativeElement.style.top = top + parseInt(this.MOVEMENT_STEP) + 'px'; this.requestAnimationFrame(this.moveSquare.bind(this)); } break; default: break; } } |
- Constructor’da, blok hareket ettirildiğinde ilgili webapi servisinin(AnimeController) çağrılması için angular tarafında yazılan services, Dependency Injection ile alınmıştır.
- “ngAfterViewInit” : Sayfa üzerindeki tüm elementler yüklendikten sonra, ilgil method çağrılır. Amaç bloğun koordinatlarının default olarak atanmasıdır.
1 2 3 4 5 6 7 8 9 |
public constructor(private service: BlockService) { } public ngAfterViewInit() { //var square = document.getElementById("square"); this.square.nativeElement.style.top = "0"; this.square.nativeElement.style.left = "0"; this.square.nativeElement.style.height = this.SQUARE_SIZE; this.square.nativeElement.style.width = this.SQUARE_SIZE; } |
Blok hareket ettirildikten sonra, SignalR Hub sınıfından tüm clientların tetikleneceği MoveBlock() function’ı aşağıdaki gibidir.
- “if (connectionId != _this._connectionId) {” : Buradaki kontrol, bloğu hareke ettirilen client kendisi ise moveSquare() methodu tekrardan tetiklenmez. Bu gelen connectionId’nin kendisininki ile aynı olup olmamasından anlaşılır.
- “_this.requestAnimationFrame” kendisine bağlanan methodu, animatif olarak çağırmaktadır. Burada çağrılan method, demin yukarıda tanımladığımız “moveSquare()” methodudur.
1 2 3 4 5 6 7 |
this._hubConnection.on('MoveBlock', (commandId: number, connectionId: string) => { if (connectionId != _this._connectionId) { console.log("Move Block :" + commandId); this.direction = commandId; _this.requestAnimationFrame(_this.moveSquare.bind(_this)); } }); |
Sayfa üzerindeki bloğun, yön tuşları ile hareket ettirildiği makalenin başında anlatılmıştı. Bu işlem sayfa üzerinde herhangi bir tuşa basıldığında tetiklenen “onkeydown()” function’ı ile yapılmaktadır. Basılan tuşun keyCode değeri, >37 ve <40 arasında ise yö ntuşlarında birine basılmış demektir. Diğer basılan tuşlar için herhangi bir işlem yapılmamaktadır.
- direction : Bloğun hareket ettirileceği yön atanır.
- _this.requestAnimationFrame(_this.moveSquare.bind(_this)) : Blok gelen komut yönünde animatif olarak hareket ettirilir.
- _this.service.moveBlock(): Bir sonraki adımda tanımlanacak service’den moveBlock() methodu, yöne ve connectionId parametreleri ile çağrılmaktadır.
1 2 3 4 5 6 7 |
window.onkeydown = function (event) { if (event.keyCode >= 37 && event.keyCode <= 40) { // is an arrow key _this.direction = event.keyCode; } _this.requestAnimationFrame(_this.moveSquare.bind(_this)); _this.service.moveBlock(_this.direction, _this._connectionId) }; |
service.ts: İlgili servicesde anime webapi servisine bağlanıp, signalR Hub sınıfına bağlı moveBlock() methodu çağrılmakta, ve böylece bloğa yapılan hareket tüm clientlara bildirmekte ve SqldB’ye loglamaktadır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; @Injectable({ providedIn: 'root' }) export class BlockService { baseUrl = "http://localhost:1923/api/anime/"; constructor(private httpClient: HttpClient) { } moveBlock(commandId: number, connectionId: string) { this.httpClient.get(this.baseUrl + `${commandId}/${connectionId}`).subscribe((res) => { //console.log(`Move:${commandId}`); }); } } |
Angular tarafında ilgili servisin kullanılabilmesi için, module’de aşağıdaki gibi tanımlanması gerekmektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { BlockService } from './services'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule ], providers: [BlockService], bootstrap: [AppComponent] }) export class AppModule { } |
Bu makalede, signalR Hub sınıfına ara bir katman oluşturularak istenen methodlar burada tanımlanmıştır. Bu şekilde ilgili hubcontext özelleştirilmiştir. Daha sonra .NetCore Webapi bir servisde, dependency injection ile ilgili SignalR hubcontext’e erişilip, Move() methodu tetiklenmiş ve yapılan hareket Sql bir DB’ye kaydedilmiştir. Ayrıca HubContext, bir sınıfa connection işlemi yapılmadan constructor’dan alınarak erişilmesi, performansda büyük bir artışa sebebiyet vermiştir. Son olarak ara bir katman kullanılarak, ilgili HubContext’in özelleştirilmesi, diğer yapılarda aynı hubcontex’in farklı methodlar ile kullanılabilmesi imkanını sağlamıştır. Bu da genişletilebilir, değiştirilmesi hızlı ve kolay olan, okunaklığı arttırılmış bir koda ulaşılmasına imkan sağlamıştır.
Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Source: https://docs.microsoft.com/tr-tr/aspnet/core/signalr/hubcontext?view=aspnetcore-2.2, https://stackoverflow.com/questions/5605588/how-to-use-requestanimationframe
Source Code : https://github.com/borakasmer/MoveBlock
.net core version’nun 3.0+ olması gerekiyor (startup daki ayarlar için) onu update etmeyi unutmayın arkadaşlar. Yazı için teşekkürler ???
https://dotnet.microsoft.com/download/thank-you/dotnet-sdk-3.0.100-preview7-macos-x64-installer
Teşekkürler, jwt token kullanıyorum. hub sınıfı içinde Login olan kullanıcı bilgisine nasıl erişebilirim?
Selamlar,
Makalesi yolda :)
Selam, sabırsızlıkla bekliyorum! :)
Makale ne zaman gelecek :((
Hangi Makale ?
jwt token kullanıyorum. hub sınıfı içinde Login olan kullanıcı bilgisine nasıl erişebilirim?
jwt token kullanıyorum. hub sınıfı içinde Login olan kullanıcı bilgisine nasıl erişebilirim?
Merhabalar Bora abi,
Bu yazıda geçen
“Önemli Not: Öncelikle eğer bir microservices yazmıyor isek, ya da yazacağımız webservices’i, ilgili signalR Hub sınıfı ile aynı sunucu üzerinde ise, yukarıdaki yöntemden başka bir yola gitmek daha doğru olacaktır. Aynı sunucu üzerinde olan signalR Hub sınıfına bağlanmak, hem performans anlamında hem de kod kalitesi açısından pek de sağlıklı değildir. Bu durumda signalR Hub sınıfının, Dependency Injection ile ilgili webapi servisinde kullanılması, performans ve kod kalitesi anlamında çok daha doğru bir yoldur.”
kısmı ile ilgili olarak yazıdaki yaklaşımın yerine IHostedService kullanımının eksi bir yönü var mıdır?