C# 8.0’dan Sonra Kodlamada Yeniliğe Gittiğim Özellikler
Selamlar,
Bu makalede .Net 8.0 ile gelen bir takım yeni özelliklerden ve .Net 6.0’dan sonra kullanmaya başladığım kod yapılarından bahsedeceğim.
Tüm denemeler Visual Studio 2022 Version: 1.6.0 Preview 1.0 ile yapılmıştır.
Ayrıca yeni bir Console Application yaratılıp, Proje çift tıklanarak alttaki gibi “<TargetFramework>net8.0</TargetFramework>” şeklinde değişikliğe gidilmiştir.
1 2 3 4 5 6 7 8 9 10 11 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <RootNamespace>Features_.Net_8._0</RootNamespace> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project> |
1-) Default Interface Methods:
Benim en çok dikkatimi çeken yeniliklerden biri de C# 8.0 ile gelen, Interface içine dolu default method yazabilme özelliğidir. Amaç Interface içinde yazılan methodların, inherit edilen sınıf içinde implemente edilmeden kullanılabilmesidir. Kısaca aslında bu yenilik, inheritance kavramına bambaşka bir boyut katmıştır. Design patternler için artık kartlar yeniden dağtılacaktır.
Aşağıdaki örnekde, Interface içini yazılan “AnalizeFile()” default methodu ile, bir mail’in body’si içindeki tüm linkler parse edilip console’a basılacaktır. Kısaca sınıfa dokunulmadan, istenen yeni özellik “AnlizeClass” sınıfına kazandırılmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
string body = @"Mail içinde geçen ilgili Urller: https://borakasmer.com, Bir başka Url: www.google.com, facebook.com, https://keepnetlabs.com/method?param=wasd, https://borakasmer.medium.com/method?param=wasd¶ms2=kjhdkjshd Bu kod body içindeki tüm Urlleri bulup konsola basmaktadır."; IAnalize analizeClass = new AnlizeClass(); analizeClass.AnalizeFile(body); Console.ReadKey(); interface IAnalize { public void AnalizeFile(string body) { Regex regx = new Regex(@"((http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?)", RegexOptions.IgnoreCase); var urls = regx.Matches(body) .OfType<Match>() .Select(m => m.Groups[0].Value) .ToArray(); Console.WriteLine("Bu Interface'e ait default Analiz Methodudur!"); foreach (var url in urls) { Console.WriteLine($".{url}"); } } } class AnlizeClass : IAnalize { } |
Yukarıdaki örneği aşağıdaki gibi değiştirdiğimizde, IAnalize interface’ine eklenen “GenrateFileName()” methodu boş olduğu için ve AnalizeClass sınıfında implemente edilmediği için, hata fırlatılmaktadır.
1 2 3 4 5 6 7 8 9 10 11 |
interface IAnalize { public void AnalizeFile(string body) { . . } public string GenarateFileName(); } class AnlizeClass : IAnalize { } |
Şimdi Gelin Interface Segregation’ı Bir Düşünelim:
Aşağıdaki örnekde IMobile interface’i, IService interface’inden türemiştir. Aslında bir class farklı görevlere ait 2 class’a ayrılırken, Interface olarak Web ve Mobile olarak ayrıştırılmıştır. IMobileService, IService’in tüm özelliklerine sahiptir. Şimdi biz IService’e, default method atarsak ne olur ? Mesela aşağıdaki örnekde:
- IService interface’ine “CheckToken()” ve “GetUserId()” default methodları tanımlanmıştır.
- IMobileService interface’inde, “CheckToken()” default methodu tekrar override edilmiştir.
- MobileService’i sınıfı, IMobileService’den türetilmiştir.
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 42 43 44 45 46 47 |
interface IService { public bool CheckToken() { return false; } public long GetUserId() { return DateTime.Now.Ticks; } } interface IMobileService :IService { public bool CheckToken() { return true; } String GetMobileDeviceId(); Boolean CheckMobileVersion(); String GetMobilePlatform(); } public class WebService : IService { public bool CheckToken() { return true; } public string GetUserId() { return "123456"; } } public class MobileService : IMobileService { public string GetMobileDeviceId() { return "352698276144152"; } public string GetMobilePlatform() { return "ios"; } public bool CheckMobileVersion() { return true; } } |
Program.cs/Main(): Aşağıdaki MobileService sınıfı, IMobileService’den türemiştir. Şimdi biz bunun “CheckToken()” methodunu çağırdığımızda, nasıl bir sonuç ile karşılaşacağız. Gerçekten kod okunaklığı şu anda, maalesef kaybolmuş durumdadır. Gelin hep beraber inceleyelim.
1 2 3 4 5 6 |
class Program { static void Main(string[] args) { IMobileService service = new MobileService(); Console.WriteLine(service.CheckToken()); } } |
- Öncelikle MobileService sınıfı “CheckToken()” methodunu implemente etmemiştir.
- IService interface’i “CheckToken()” methodunu, default method olarak tanımlamıştır.
- Son olarak IMobileService interface’i de “CheckToken()” default methodunu override etmiştir. Demek ki son olarak, IMobileServisine ait olan “CheckToken()” methodu çalışacak ve “true” değeri geriye dönülecektir.
Default Interface Methodu’nun esas amacı, eski kodları değiştirmeden sınıflara yeni özelliklerin getirilebilmesidir. Örneğin, ortak “IAnalize” interface’den türemiş “UrlAnalize” ve “AttachmentAnalize” sınıflarımız olsun. Bu sınıfları bozmadan sisteme yeni dahil olan yine “IAnalize“‘dan türemiş “ImageAnalize” sınıfımızın, “CheckSize()” adında yeni bir methoda ihtiyacı olsun. Eski yol ile, bu sınıf için yeni bir Interface tanımlayıp (“IImageAnalize“) “ImageAnalize“‘ı hem bu yeni “IImageAnalize” Interfaceinden hem de “IAnalize“‘dan türetebilirz. [ImageAnalize : IAnalize, IImageAnalize] Ama var olan sınıfların da, bu “CheckSize()” methodunu kullanmasını ister isek, ya tek tek hepsine bu methodu tanımlayacağız, ya da “IAnalize” interface’ine Default Method olarak “CheckSize()” methodunu ekleyeceğiz. Böylece, önceden yazılmış kodlarda hiçbir değişikliğe gitmeden, ilgili eklentiyi yapılmış olacağız.
2-) Readonly Struct:
Aşağıdaki örnekte Struct içinde atanan değerler, sadece ilk sefer Constructor’da atanmakda ve bir daha değiştirilememektedirler. Böylece Immutable Struct oluşturulmuştur. Burada amaç, başta atanan propertylerin bir daha değiştirilmeden, sonuçlarının farklı zamanlarda tekrardan alınmasıdır.
1 2 3 4 5 6 7 8 9 10 11 12 |
public readonly struct Rectangle { public readonly double Height { get; } public readonly double Width { get; } public double Area => (Height * Width); public Rectangle(double height, double width) { Height = height; Width = width; } public override string ToString() { return $"(Toplam alan Yükseklik: {Height}cm, Genişlik: {Width}cm) için {Area}'dır"; } } |
Aşağıda Rectangle Struct’ının Height ve Width propertyleri ilk ayağa kaldırılken constructor’da set edilmiş, ve daha sonra istenen alan bilgisi çekilmiştir. Aynı alan bilgisi, projenin başka yerlerinde de değişmeden kullanılabilecektir.
1 2 3 4 5 6 7 |
Rectangle rectangle = new Rectangle(10, 20); Console.WriteLine("Height(cm): " + rectangle.Height); Console.WriteLine("width(cm): " + rectangle.Width); Console.WriteLine("Rectangle Area: " + rectangle.Area); Console.WriteLine("Rectangle: " + rectangle); //rectangle.Width = 15; Console.ReadKey(); |
3-) Using Declarations:
Aşağıdaki örnekde “using” süslü parantezler olmadan basit bir şekilde kullanılmıştır.=> “using StreamReader file = new StreamReader(filePath)“. Amaç okunabilirliği arttırmaktır.
Alttaki “WriteLinesFromFile()” methodu ile, belirtilen “.txt” bir dosya, satır satır Console’a basılmaktadır.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
List<string> WriteLinesFromFile(string filePath) { using StreamReader file = new StreamReader(filePath); string line; List<string> rows = new(); while ((line = file.ReadLine()) != null) { rows.Add(line); }; return rows; } foreach (var row in WriteLinesFromFile("C:\\Projects\\test.txt")) { Console.WriteLine(row); } |
4-) Static Local Functions Ve Switch Expression :
C# 7.0 ile gelen Local functions, C# 8.0 ile artık Static olarak da tanımlamabilmektedir. Aşağıdaki örnekde, girilen geometri tipine ve boyutlara göre, alanı hesaplanmaktadır. Ayrıca Switch Expression, bence bugüne kadarki olabilecek en sade ve anlaşılır kodu bize sunmaktadır.
- ShapeTypes Enum ile, belirli Geometrik şekillerin alanı hesaplanmaktadır.
- CalculateShape() static methodu içinde => “static void CalculateArea(ShapeTypes type, int Num1, int Num2)” methodu local olarak tanımlanmıştır. Böylece bu local function içinde tanımlı hiçbir parametreye, dışardan erişilememektedir.
- “var area = type switch {“: C# 8.0 ile gelen Switch Expression ile, kod okunaklığı arttırılmış ve sadeleştirilmiştir.
- “ShapeTypes.Kare => Num1 * Num2“: İlgili Geometrik şeklin Kare olması durumundaki alan hesabı, sade bir şekilde tanımlanmıştır. Benzer işlemler, “Daire” ve “Ucgen” için de yapılmıştır.
- “_ => 0”: Bilinmeyen bir şekil parametrik olarak verildiğinde, bu default seçeneğine gelinmekte ve alan için “0” değeri atanmaktadı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 |
public enum ShapeTypes { Kare = 1, Daire = 2, Ucgen = 3 } class Program { static void Main(string[] args) { CalculateShape(); } public static void CalculateShape() { int X = 20, Y = 30; CalculateArea(ShapeTypes.Kare, X, Y); static void CalculateArea(ShapeTypes type, int Num1, int Num2) { var area = type switch { ShapeTypes.Kare => Num1 * Num2, ShapeTypes.Daire => Math.Pow(Num1,2) * Math.PI, ShapeTypes.Ucgen => Num1 * Num2 / 2, _ => 0 }; Console.WriteLine($"Num1 = {Num1}cm, Num2 = {Num2}cm, {type}=> Alan = {area}cm^2"); } CalculateArea(ShapeTypes.Daire, 30, 10); CalculateArea(ShapeTypes.Ucgen, 80, 60); } } |
Sonuç:
5-) Asynchronous Streams:
Artık “foreach“ loopları async tanımlaması ile, asenkron olarak çağıra bilmekteyiz. Aşağıdaki methodda ilk göze çarpan, geri dönüş tipi “IAsyncEnumerable<int>“‘dir. Ayrıca “yield return” keyword’ü ile her üretilen değer, asenkron olarak loop’dan çıkılmadan teker teker geri dönülmektedir. Her bir loop arasına “await Task.Delay(100)” 100ms bekleme konulmuştur.
1 2 3 4 5 6 7 |
public static async IAsyncEnumerable<int> GetRandomNumber(int count) { var random = new Random(); for(int i = 0; i < count; i++) { await Task.Delay(100); yield return random.Next(1000); } } |
Şimdi sıra geldi bu methodu bir loop içinde asenkron olarak console’a basmaya: Aşağıdaki kodda, en önemli kısım “await foreach()” kullanım şeklidir. Böylece Stream şeklinde gelen akış, console’a asenkron olarak basılmaktadır.
1 2 3 4 5 |
static async Task Main(string[] args) { await foreach (var randomNumber in GetRandomNumber(20)) { Console.WriteLine(randomNumber.ToString()); } } |
6-) Tanımlı Olmayan Tipler İçin Raw SQL Queryler .NET 8.0:
.Net 8.0 ile hayatımıza girecek olan ve bence geç kalınmış bir Entity özelliğidir. Artık Dapper’a hiç ihtiyaç kalmayacaktır. Öncelikle DAL Class Library projesi yaratılır. Sonra aşağıdaki kütüphaneler Nuget’den indirilir.
Aşağıdaki komut ile Test DB’sinden TestContext ve User ile UserAddress Entityleri, DBFirst ile aşağıdaki scaffod komutu sayesinde otomatik oluşturulur.
1 2 3 |
dotnet ef dbcontext scaffold "Server=.;Database=Test;Trusted_Connection=True; TrustServerCertificate=True;" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context-dir "Entities\DBContexts" --no-pluralize -c TestContext -f |
MsSql TestDB yapısı, aşağıdaki gibidir:
UserData: Custom query sonucu olarak geri dönülecek DB’de karşılığı olmayan, “CustomDataModel” aşağıdaki gibidir.
1 2 3 4 5 6 7 8 |
public class UserData { public int UserID { get; set; } public string Name{ get; set; } public string Surname { get; set; } public int? AddressID { get; set; } public string? Address { get; set; } public bool IsDeleted { get; set; } } |
Program.cs: Aşağıda görüldüğü gibi ilk önce TestContext yaratılmıştır.
- “var services = new ServiceCollection()“: Servis collection oluşturulur.
- “var serviceProvider = services.BuildServiceProvider()“: Burada Dependency Injection kullanılmamıştır.
- “services.AddDbContext<TestContext>“: Burada TestContext database Connection string ile oluşturulmuştur. İlgili connection’ın, profesyonel hayatta configden encrypted olarak alınması gerekir :)
- “var serviceProvider = services.BuildServiceProvider()”: Service Provider oluşturulmuştur.
- “_testDbContext = serviceProvider.GetService<TestContext>()”: TestDBContext ayağı kaldırılır.
- “await _testDbContext.Database.SqlQuery<UserData>()“: UserData, User ve Address entitylerinin join’i ile oluşturulmuş DB’de karşılığı olmayan DataView modeldir. Yukarıda, ilgili Model’in propertyleri tanımlanmıştır. Yazılan RawSql sonucu, asenkron olarak beklenmektedir.
- “select US.Id as UserID, us.Name,us.Surname, ua.Id as AddressID, ua.Address,us.IsDeleted from [dbo].[User] as us left join [dbo].[UserAddres] as ua on us.Id =ua .UserId“: Bu query’de, Userların tamamı ve varsa user Adres bilgisi left join ile alınmıştır.
- “.Where(u => u.IsDeleted == false) .ToListAsync()”: Where koşulu ve result’ın ListAsync olarak dönmesi sağlanmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Program { private static TestContext _testDbContext; static async Task Main(string[] args) { var services = new ServiceCollection(); services.AddDbContext<TestContext>(options => options.UseSqlServer("Server=.;Database=Test;Trusted_Connection=True;TrustServerCertificate=True;")); var serviceProvider = services.BuildServiceProvider(); _testDbContext = serviceProvider.GetService<TestContext>(); var users = await _testDbContext.Database.SqlQuery<UserData>( @$"select US.Id as UserID, us.Name,us.Surname, ua.Id as AddressID, ua.Address,us.IsDeleted from [dbo].[User] as us left join [dbo].[UserAddres] as ua on us.Id =ua .UserId") .Where(u => u.IsDeleted == false) .ToListAsync(); foreach (var user in users) { Console.WriteLine($"[{user.UserID}] - {user.Name} {user.Surname}=> Adres : {user.Address}"); } } |
Sonuç:
Aslında RawSql, dinamik Query ihtiyacında, tam bir can simidi olmuştur. Örneğin paramterik gelen Tablo veya DB isimlerinin, Linq Query ile yazılması pek mümkün değildir. Bu durumda RawSqller devreye girer. .Net Core 2.0’a kadar Anonymous tipde RawSql yazmak desteklenirken .Net Core 3.1 itibari ile bu destek ortadan kaldırılmıştır. Taa ki çıkıcak olan .Net 8.0’a kadar. Sanırım Linq Query haricinde, RawSql halen eski populerliğini korumakta ve DB karşılığı olmayan, birçok tablo ile Joinli olan sorgu sonuçları artık Entity 8.0 ile desteklenmektedir. .Net 7.0 ve öncesi için Custom çözümleri bu makaleden erişebilirsiniz.
Daha bahsetmediğim ve kullandığım birçok yenilik bulunmaktadır. Ama makalenin de bir sınırı olduğundan, 6.Maddede kesmek zorunda kaldım. Belki birkaç farklı makale ile diğer özelliklerden de ilerde bahsederim. Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Kaynaklar:
Emeğine sağlık Bora hocam
Tesekkurler…
Hocam Başlık Olarak c# 8 kullanmışsınız c# 12 ve .net 8 olmayacakmı ?, anlayan anlamıştır tabikide yinede düzeltmekte fayda var diye düşünüyorum.
Merhaba bora hocam default interface methods o kadar saçma geldi ki anlatamam. Interface amacı contracttır ama bu srp ayrı bir halde işte yapıyor. Tek görevi contract tutmak olmalı. Eee interface ile abstract classın ne farkı var şimdi? Bu tarz problemler hep abstract class üzerinden çözülmeliydi.