Bu bölüm Serenity v5 için güncellenecektir.
Serenity'de Hizmet Uç Noktaları, ASP.NET MVC denetleyicilerinin bir alt sınıfıdır.
Northwind OrderEndpoint'ten bir alıntı:
namespace Serene.Northwind.Endpoints
{
[RoutePrefix("Services/Northwind/Order"), Route("{action}")]
[ConnectionKey("Northwind"), ServiceAuthorize(Northwind.PermissionKeys.General)]
public class OrderController : ServiceEndpoint
{
[HttpPost]
public SaveResponse Create(IUnitOfWork uow, SaveRequest<MyRow> request)
{
return new MyRepository().Create(uow, request);
}
public ListResponse<MyRow> List(IDbConnection connection, ListRequest request)
{
return new MyRepository().List(connection, request);
}
}
}
Denetleyici Adlandırma ve Ad Alanı
Sınıfımızın dosyası OrderEndpoint.cs olmasına rağmen OrderController adını taşıyor . Bunun nedeni, tüm denetleyicilerin Denetleyici sonekiyle bitmesi gereken bir ASP.NET MVC sınırlamasıdır (ki bunu pek mantıklı bulmuyorum) .
Denetleyici sınıfı adınızı bu son ekle bitirmezseniz eylemleriniz işe yaramayacaktır. Bu yüzden bu konuda çok dikkatli olun.
Ben de bu hatayı birkaç kez yaptım ve bana saatler kaybettirdi.
Bu sınıfın ad alanı ( Serene.Northwind.Endpoints ) hiç önemli değil, ancak tutarlılık için genellikle uç noktaları MyProject.Module.Endpoints ad alanının altına koyarız.
OrderController'ımız, bu MVC denetleyicisine birazdan göreceğimiz pek de alışılmış olmayan özellikler sağlayan ServiceEndpoint'ten (ve olması gereken) türetilmiştir.
Yönlendirme Nitelikleri
[RoutePrefix("Services/Northwind/Order"), Route("{action}")]
ASP.NET öznitelik yönlendirmesine ait olan yukarıdaki yönlendirme öznitelikleri, hizmet uç noktamız için temel adresi yapılandırır. Eylemlerimize "mysite.com/Services/Northwind/Order" adresinden ulaşılabilir.
Lütfen ApplicationStart yöntemindeki tüm rotaları Route.AddRoute vb. ile yapılandırdığınız klasik ASP.NET MVC yönlendirmesinden kaçının. Yönetilmesi gerçekten zordu.
Tüm Serenity hizmeti uç noktaları, varsayılan olarak /Services/Module/Entity adresleme şemasını kullanır. Yine başka bir adres şeması kullanabilecek olsanız da tutarlılık ve temel kurallar açısından bu önerilir.
ConnectionKey Özelliği
Bu öznitelik, gerektiğinde bir bağlantı oluşturmak için uygulama yapılandırma dosyanızdaki (örn. web.config) hangi bağlantı anahtarının kullanılması gerektiğini belirtir.
Otomatik olarak oluşturulan bu bağlantının ne zaman ve nasıl kullanıldığını görelim:
public ListResponse<MyRow> List(IDbConnection connection, ListRequest request)
{
return new MyRepository().List(connection, request);
}
Burada bu eylemin IDbConnection parametresini aldığını görüyoruz. İstemci tarafından bir MVC eylemine IDbConnection gönderemezsiniz. Peki bu bağlantıyı kim yaratıyor?
Denetleyicimizin ServiceEndpoint'ten türetildiğini hatırlıyor musunuz? Dolayısıyla ServiceEndpoint, eylemimizin bir bağlantı gerektirdiğini anlıyor. Bağlantı anahtarını belirlemek için denetleyici sınıfının üstündeki [ConnectionKey] niteliğini kontrol eder, SqlConnections.NewByKey() kullanarak bağlantı oluşturur , bu bağlantıyla eylemimizi yürütür ve eylemin yürütülmesi sona erdiğinde bağlantıyı kapatır.
Bu bağlantı parametresini eylemden kaldırabilir ve manuel olarak oluşturabilirsiniz:
public ListResponse<MyRow> List(ListRequest request)
{
using (var connection = SqlConnections.NewByKey("Northwind"))
{
return new MyRepository().List(connection, request);
}
}
Aslında ServiceEndpoint'in perde arkasında yaptığı da budur.
Platform bu ayrıntıyı otomatik olarak ele alırken neden bu özelliği kullanmıyorsunuz? Bunun bir nedeni, yapılandırma dosyasında listelenmeyen özel bir bağlantı açmanız veya bazı koşullara bağlı olarak dinamik bir bağlantı açmanız gerektiği durumlar olabilir.
IDbConnection parametresi yerine IUnitOfWork (işlem) alan başka bir yöntemimiz var:
public SaveResponse Create(IUnitOfWork uow, SaveRequest<MyRow> request)
{
return new MyRepository().Create(uow, request);
}
Burada da durum benzer. ServiceEndpoint yeniden bir bağlantı oluşturur, ancak bu sefer üzerinde bir işlem başlatır (IUnitOfWork), eylem yöntemimizi çağırır ve geri döndüğünde işlemi otomatik olarak gerçekleştirecektir. Yine başarısız olursa geri alırdı.
İşte aynı şeyin manuel versiyonu:
public SaveResponse Create(SaveRequest<MyRow> request)
{
using (var connection = SqlConnections.NewByKey("Northwind"))
using (var uow = new UnitOfWork(connection))
{
var result = new MyRepository().Create(uow, request);
uow.Commit();
return result;
}
}
Yani ServiceEndpoint, 1 satır kodda 8 satır alan bir şeyi yönetir.
IUnitOfWork / IDbConnection Ne Zaman Kullanılır?
Geleneksel olarak, bazı durumları değiştiren (CREATE, UPDATE vb.) Serenity eylem yöntemleri bir işlem içinde çalışmalı, dolayısıyla bir IUnitOfWork parametresi almalı ve salt okunur işlemler (LIST, RETRIEVE) olanlar IDbConnection'ı kullanmalıdır.
Hizmet yönteminiz bir IUnitOfWork parametresi alıyorsa bu, yönteminizin bazı verileri değiştirebileceğinin bir imzasıdır.
[HttpPost] Özelliği Hakkında
Create, Update, Sil vb. yöntemlerin bu özniteliğe sahip olduğunu ancak List, Retrieve vb. yöntemlerin olmadığını fark etmiş olabilirsiniz.
Bu özellik Oluşturma, Güncelleme ve Silme işlemlerini yalnızca HTTP POST ile sınırlar. HTTP GET tarafından çağrılmalarına izin vermiyor.
Bunun nedeni, bu yöntemlerin bazı durumları değiştiren yöntemler olmasıdır, örneğin bazı kayıtları DB'ye ekleme, güncelleme, silme gibi. Bu nedenle, bunların yanlışlıkla çağrılmasına ve sonuçlarının önbelleğe alınmasına izin verilmemelidir.
Bunun aynı zamanda bazı güvenlik sonuçları da var. GET yöntemiyle yapılan işlemler bazı saldırılara maruz kalabilmektedir.
List, Retrieve hiçbir şeyi değiştirmez, bu nedenle GET ile çağrılmalarına, örneğin tarayıcı adres çubuğuna yazmalarına izin verilir.
List, Retrieve GET tarafından çağrılabilse de Serenity, Q.CallService gibi yöntemlerini kullandığınızda her zaman HTTP POST kullanarak hizmetleri çağırır ve beklenmedik sonuçları önlemek için önbelleğe almayı kapatır.
ServiceYetkilendirme Özelliği
Denetleyici sınıfımızın ServiceAuthorize özelliği var:
ServiceAuthorize(Northwind.PermissionKeys.General)
Bu öznitelik, ASP.NET MVC [Yetkilendir] özniteliğine benzer, ancak yalnızca kullanıcının oturum açmış olup olmadığını kontrol eder ve aksi takdirde bir istisna atar.
Hiçbir parametre olmadan kullanılırsa, örneğin [ServiceAuthorize()] bu özellik aynı zamanda kullanıcının oturum açıp açmadığını da kontrol eder.
Bir izin anahtarı dizesini ilettiğinizde, kullanıcının oturum açmış olduğunu ve ayrıca bu izne sahip olup olmadığını kontrol edecektir.
ServiceAuthorize("SomePermission")
Kullanıcıya "SomePermission" verilmezse, bu onun herhangi bir uç nokta yöntemini yürütmesini engelleyecektir.
Benzer şekilde çalışan [PageAuthorize] özelliği de vardır ancak hata işlemesi hizmetler için daha uygun olduğundan hizmet uç noktalarıyla [ServiceAuthorize]'ı tercih etmelisiniz.
[PageAuthorize] kullanıcıyı Giriş sayfasına yönlendirirken eğer kullanıcı izne sahip değilse ServiceAuthorize daha uygun NotAuthorized hizmet hatası döndürmektedir .
Eylemlerde denetleyici yerine [ServiceAuthorize] niteliğini kullanmak da mümkündür:
[ServiceAuthorize("SomePermissionThatIsRequiredForCreate")]
public SaveResponse Create(SaveRequest<MyRow> request)
İstek ve Yanıt Nesneleri Hakkında
Özel olarak işlenen IUnitOfWork ve IDbConnection parametreleri dışında, tüm Serenity hizmeti eylemleri tek bir istek parametresi alır ve tek bir sonuç döndürür.
public SaveResponse Create(IUnitOfWork uow, SaveRequest<MyRow> request)
Sonuçla başlayalım. ASP.NET MVC hakkında biraz bilginiz varsa denetleyicilerin rastgele nesneler döndüremeyeceğini bilirsiniz. ActionResult'tan türetilen nesneleri döndürmeleri gerekir .
Ancak SaveResponse'umuz sıradan bir nesne olan ServiceResponse'dan türetilmiştir :
public class SaveResponse : ServiceResponse
{
public object EntityId;
}
public class ServiceResponse
{
public ServiceError Error { get; set; }
}
Bu nasıl mümkün olabilir? ServiceEndpoint yine bu ayrıntıyı perde arkasında ele alıyor. SaveResponse'umuzu JSON verilerini döndüren özel bir eylem sonucuna dönüştürür.
Yanıt nesnemiz ServiceResponse'tan türetildiği ve JSON serileştirilebilir olduğu sürece bu ayrıntı hakkında endişelenmemize gerek yok.
Yine istek nesnemiz de temel bir ServiceRequest'ten türetilen sıradan bir sınıftır:
public class SaveRequest<TEntity> : ServiceRequest, ISaveRequest
{
public object EntityId { get; set; }
public TEntity Entity { get; set; }
}
public class ServiceRequest
{
}
ServiceEndpoint, genellikle JSON olan HTTP istek içeriğini alır, özel bir MVC eylem filtresi (JsonFilter) kullanarak onu istek parametremize seri durumdan çıkarır.
Bazı özel eylemler kullanmak istiyorsanız, yöntemleriniz de bu felsefeyi takip etmelidir; örneğin yalnızca bir istek almalı (ServiceRequest'ten türetilerek) ve bir yanıt döndürmelidir (ServiceResponse'dan türeterek).
Belirli bir miktardan büyük tüm siparişlerin sayısını döndüren bir hizmet yöntemi ekleyelim:
public class MyOrderCountRequest : ServiceRequest
{
public decimal MinAmount { get; set; }
}
public class MyOrderCountResponse : ServiceResponse
{
public int Count { get; set; }
}
public class OrderController : ServiceEndpoint
{
public MyOrderCountResponse MyOrderCount(IDbConnection connection,
MyOrderCountRequest request)
{
var fld = OrderRow.Fields;
return new MyOrderCountResponse
{
Count = connection.Count<OrderRow>(fld.TotalAmount >= request.MinAmount);
};
}
}
Lütfen bu modeli takip edin ve eylem yöntemlerine daha fazla parametre eklememeye çalışın. Serenity, daha sonra daha fazla özellik eklenerek genişletilebilecek tek bir istek nesnesiyle mesaj tabanlı modeli izler.
Bunu yapmayın (buna RPC - Uzaktan prosedür çağrısı stili denir):
public class OrderController : ServiceEndpoint
{
public decimal MyOrderCount(IDbConnection connection,
decimal minAmount, decimal maxAmount, ....)
{
// ...
}
}
Bunu tercih edin (mesaj tabanlı hizmetler):
public class MyOrderCountRequest : ServiceRequest
{
public decimal MinAmount { get; set; }
public decimal MaxAmount { get; set; }
}
public class OrderController : ServiceEndpoint
{
public MyOrderCountResponse MyOrderCount(IDbConnection connection,
MyOrderCountRequest request)
{
// ...
}
}
Bu, parametre sırasını hatırlama zorunluluğunu ortadan kaldıracak, istek nesnelerinizi geriye dönük uyumluluğu bozmadan genişletilebilir hale getirecek ve daha sonra fark edebileceğiniz daha birçok avantaja sahip olacaktır.
Uç Nokta Yöntemleri Neden Neredeyse Boş?
Genellikle fiili çalışmayı depo katmanımıza devrederiz:
public ListResponse<MyRow> List(IDbConnection connection, ListRequest request)
{
return new MyRepository().List(connection, request);
}
ServiceEndpoint'in ASP.NET MVC'ye doğrudan bağımlı olduğunu unutmayın. Bu, bir hizmet uç noktasının içine yazdığınız herhangi bir kodun ASP.NET MVC'ye ve dolayısıyla web ortamına bağımlı olacağı anlamına gelir.
Diyelim ki bir masaüstü uygulamasından buraya yazdığınız herhangi bir kodu yeniden kullanamayabilirsiniz veya bu kodu WEB kitaplıklarına referansı olmayan bir DLL'ye ayıramayabilirsiniz.
Ancak gerçekten böyle bir gereksiniminiz yoksa, depoları hep birlikte kaldırabilir ve tüm kodunuzu uç noktanın içine yazabilirsiniz.
Bazı kişiler varlıkların, depoların, iş kurallarının, uç noktaların vs. hepsinin kendi yalıtılmış düzenekleri içinde olması gerektiğini savunabilir. Teorik olarak ve bazı senaryolar için bu geçerli olabilir, ancak bazı (veya çoğu) kullanıcıların çok fazla izolasyona ihtiyacı yoktur ve YAGNI (buna ihtiyacınız olmayacak) kategorisine girebilir.