.Net Core ve Go ile Mikroservis Mimarisi
Selamlar,
Bu makalede, .Net Core bir WebApi projesinde bir şirket yaratılacak ve bu şirket Go ile yazılmış bir Microservices tarafından işlenmek üzere bir kuyruğa alınacaktır. Peki neden Microservice ? Çünkü yapılacak işlemler, vakit alan uzun süren işlemlerdir. Ayrıca fazlaca sistem tüketen işlerdir. Bu nedenle ilgili işlemleri, harici bir makinada ve asenkron olarak işletmek için Microservicesler tercih edilmiştir.
.Net Core WebApi:
Gelin ilk önce .Net Core bir WebApi projesini, aşağıdaki gibi yaratalım.
.Net Versiyonu olarak aşağıda görüldüğü gibi .Net 5.0 seçilmiştir.
Nuget Package Manager’dan, proje için gerekli olan aşağıdaki dosyalar indirilir:
Yaratılacak olan Company Model, aşağıdaki gibidir.
1 2 3 4 5 6 7 |
public class Company { public string CompanyName { get; set; } public float CompanyCode { get; set; } public string CompanyUrl { get; set; } public string ApiUrl { get; set; } } |
Bugünün sorunları dünün çözümlerinden kaynaklanır. ―Peter M. Senge
Startup.cs(ConfigureServices()): .Net Core Web Api projesinde Swagger entegrasyonu için Startup.cs, aşağıdaki gibi değiştirilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public void ConfigureServices(IServiceCollection services) { services.AddSwaggerGen(c => { c.EnableAnnotations(); c.SwaggerDoc("CoreSwagger", new OpenApiInfo { Title = "Swagger on ASP.NET Core", Version = "1.0.0", Description = "VBT Web Api", TermsOfService = new Uri("http://swagger.io/terms/") }); }); services.AddControllers(); } |
Startup.cs(Configure()): Swagger için ilgili “.json”, aşağıdaki gibi tanımlanır.
1 2 3 4 5 6 7 |
. . app.UseSwagger() .UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/CoreSwagger/swagger.json", "Swagger Test .Net Core"); }); |
CompanyController: Aşağıda görüldüğü gibi, InsertCompany() methodunda Company modeli, parametre olarak beklenmektedir.
- SwaggerOperation: İlgili methodun, swagger üzerindeki görünen açıklamasıdır. Bunun için “Swashbuckle.AspNetCore.Annotations” kütüphanesinin, indirilmesi gerekmektedir.
- “var factory = new ConnectionFactory()”: RabbitMQ’ya bağlanılacak configurasyon tanımlanır.
- “channel.QueueDeclare(queue: “company”: RabbitMQ’ya atılacak Company channel, burada tanımlanır.
- “var companyData = JsonConvert.SerializeObject(companyModel)”: Gelen CompanyModel, Serialize edilip daha sonra byte[] dizisine çevrilir.
- “channel.BasicPublish(exchange: “”, routingKey: “company”, basicProperties: null, body: body)”: Gönderilen model, “Company” channel’ına konur. Burada amaç, sonuçlanması uzun sürecek işleri client’ı bekletmeden arkada yapmak ve sunucu üzerindeki yükü başka makinalara aktarmaktı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 30 31 32 33 34 35 36 37 38 39 40 |
[HttpPost] [SwaggerOperation(Summary = "Add New CompanyName To The RabbitMQ.", Description = "<h2>Trigger Go Microservice</h2> <br> When You Add new Company to the Queue, It will insert to the SQLDB and Get Latest 10 Articles from borakasmer.com")] public bool InsertCompany(Company companyModel) { try { var factory = new ConnectionFactory() { HostName = "127.0.0.1", Password = "guest", Port = 5672, VirtualHost = "/", }; using (var connection = factory.CreateConnection()) using (var channel = connection.CreateModel()) { channel.QueueDeclare(queue: "company", durable: false, exclusive: false, autoDelete: false, arguments: null); var companyData = JsonConvert.SerializeObject(companyModel); var body = Encoding.UTF8.GetBytes(companyData); channel.BasicPublish(exchange: "", routingKey: "company", basicProperties: null, body: body); Console.WriteLine($"{companyModel.CompanyName} is Send to the queue"); } return true; } catch (Exception ex) { Console.WriteLine(ex.Message); return false; } } |
Go Microservice:
Sıra geldi, bu channel’ı dinleyen bir Go Microservices’i yazmaya. İlgili CompanyModel channel’a düştüğünde, Remotedaki bir SQLDB’ye ilgili Company kaydı yazılıp, bunun haricinde “borakasmer.com” sayfası pars edilip son 10 makalesi, console’a örnek amaçlı yazdırılacaktır. Aşağıda, Go projesinde kullanılan sayfalar, görevlerine göre ayrılmıştır.
Aklınıza neden Go diye bir soru gelir ise, hem mikroservices projerinde tüketilen kaynağın azlığı, hem çalışma performansı hem de kodlama kolaylığından dolayı tercih edilmiştir.
shared.go: Proje içinde kullanılacak tüm tanımlamaların yapıldığı, package’dır. Aşağıda dikkat ederseniz, Channel’a atılan CompanyModel, uzak bağlantı kurulacak SQLDB’nin ve RabbitMQ’nun connection stringleri tanımlanmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package shared import "time" type Configuration struct{ AMQPURL string SQLURL string } var Config = Configuration{ AMQPURL: "amqp://guest:guest@localhost:5672/", SQLURL: "sqlserver://accountinguser:testUser@192.168.1.1?database=CompanyBlog&connection+timeout=30", } type AddCompany struct { CompanyName string CompanyCode float64 CompanyUrl string ApiUrl string CreatedDate time.Time UpdateDate time.Time IsDeleted bool } |
İnsanların yanılgısı, karşılaştıkları sorunları tam olarak açıklamadan önce, çözümleri istemeleridir. ―Jorge Angel Livraga
sql.go: Gelen CompanyModel’in Remote’daki bir SQLDB kaydedilmesi ve var olan Company kayıtların daha en başta listelenmesi için kullanılan package’dır.
- “func GetSqlContent(db *sql.DB)”: Kayıtlı company listesinin çekildiği functiondır.
- “CompanyName []string CompanyCode []float64 CompanyUrl []string ApiUrl []string” : SqlDB’den çekilecek olan kolonlar, dizi haline bu değişkenlere atanacaktır.
- “ctx”: Go’da Sql Query çekildiği zaman, time out süresinin tanımlanması için kullanılmıştır. Aksi durumda, Sql Query’de bir sorun olması durumunda query sonlandırılmaz.
- “rows, err := db.QueryContext(ctx, “select CompanyName,CompanyCode,CompanyUrl,ApiUrl from [dbo].[Companies] where IsDeleted = 0″)”: Companies tablosundan, ilgili kolonlar çekilmiş ve rows değişkenine atanmıştır.
- “for rows.Next() {” : Herbir kayıt, tek tek gezilir.
- “err := rows.Scan(&_companyName, &_companyCode, &_companyUrl, &_apiUrl)”: Her satıra ait kolonlar, tanımlı ilgili değişkenlere atanır.
- “CompanyName = append(CompanyName, _companyName)”: Herbir değişken, tanımlı kolonlara ait dizilere atanır.
- “func InsertSqlContent(db *sql.DB, company *shared.AddCompany)”: Yeni bir Company kaydının, DB’ye atılması için kullanılan functiondır.
- “stmt, err := db.Prepare(“INSERT INTO Companies(CompanyName,CompanyCode,CompanyUrl,ApiUrl) VALUES (@p1, @p2,@p3,@p4); select ID = convert(bigint, SCOPE_IDENTITY())”)”: Companies tablosuna kaydedilen Insert query’sidir. Geriye insert edilen kaydın ID değeri, “Scope_Identity” ile alınmaktadır.
- “ctx, cancel := context.WithTimeout(context.Background(), time.Minute)”: Insert işlemi için DB’ye, default 1 dakkalık connection timeout süresi atanmaktadır.
- “defer stmt.Close()”: Enson işlem bitince, Sql connectionın kapanması için çağrılan functiondır.
- “rows := stmt.QueryRowContext(ctx, company.CompanyName, company.CompanyCode, company.CompanyUrl, company.ApiUrl)”: İlgili insert işlemi için yukarıda tanımlanan (@p1, @p2,@p3,@p4) parametrelerine Gönderilen Company’nin kolonları atanır ve ilgili Insert Query çalıştırılır.
- “var _id int64 rows.Scan(&_id) return _id, nil”: Insert sonucu dönen identity ID değeri, “_id” değişkenine atanarak geri dönülü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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
package sql import ( "Test/shared" "context" "database/sql" _ "github.com/denisenkom/go-mssqldb" "log" "time" ) func SqlOpen() *sql.DB { db, err := sql.Open("sqlserver", shared.Config.SQLURL) if err != nil { log.Fatal(err) } return db } func GetSqlContent(db *sql.DB) ([]string, []float64, []string, []string, error) { var ( CompanyName []string CompanyCode []float64 CompanyUrl []string ApiUrl []string ctx context.Context ) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() rows, err := db.QueryContext(ctx, "select CompanyName,CompanyCode,CompanyUrl,ApiUrl from [dbo].[Companies] where IsDeleted = 0") if err != nil { log.Fatal(err) } defer rows.Close() for rows.Next() { var _companyName string var _companyCode float64 var _companyUrl string var _apiUrl string err := rows.Scan(&_companyName, &_companyCode, &_companyUrl, &_apiUrl) if err != nil { return CompanyName, CompanyCode, CompanyUrl, ApiUrl, err } else { CompanyName = append(CompanyName, _companyName) CompanyCode = append(CompanyCode, _companyCode) CompanyUrl = append(CompanyUrl, _companyUrl) ApiUrl = append(ApiUrl, _apiUrl) } } return CompanyName, CompanyCode, CompanyUrl, ApiUrl, nil } func InsertSqlContent(db *sql.DB, company *shared.AddCompany) (int64, error) { stmt, err := db.Prepare("INSERT INTO Companies(CompanyName,CompanyCode,CompanyUrl,ApiUrl) VALUES (@p1, @p2,@p3,@p4); select ID = convert(bigint, SCOPE_IDENTITY())") if err != nil { handleError(err, "Could not insert SqlDB") return 0, err } var ctx context.Context ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() defer stmt.Close() rows := stmt.QueryRowContext(ctx, company.CompanyName, company.CompanyCode, company.CompanyUrl, company.ApiUrl) if rows.Err() != nil { return 0, err } var _id int64 rows.Scan(&_id) return _id, nil } func handleError(err error, msg string) { if err != nil { log.Fatalf("%s: %s", msg, err) } } |

borakasmer.com’da ilgili maklelerin başlık ve urllerinin bulunması için “entery-title” css’i, filitre olarak kullanılmıştır.
parser.go: “borakasmer.com”‘un son 10 makalesinin, parse edilip çekildiği package’dır. Bu işlem için Go’da “PuerkitoBio/goquery” paketi kullanılmıştır.
- “var articleList map[string]string”: Bir çeşit dictionary olan map ile, çekilen makalelerin başlık ve urlleri bu articleList’e doldurulur.
- “client := &http.Client{ Timeout: 30 * time.Second, }”: Request çekilecek site için, 30sn’lik request timeout süresi tanımlanmıştır.
- “res, err := client.Get(“https://www.borakasmer.com”)”: İlgili sitete request çekilir.
- “defer res.Body.Close()”: Tüm işlemler bittikten sonra, connection’ın kapanması için ilgili function çağrılmıştır.
- “doc, err := goquery.NewDocumentFromReader(res.Body)”: Request’den dönen sonuc, yani sayfanın body’si doc değişkenine atanmıştır.
- “data := doc.Find(“.entry-title”)”: İlgili sayfa içinde “entry-title” ile başlayan tüm HtmlElementler filitrelenmiştir. Bu şekilde, ilgili makalelerin tümü bulunmuş olmaktadır.
- “articleList = make(map[string]string, data.Length())”: Bulunan makale sayısı kadar articleList’e, boyut atanarak “make()” komutu ile yaratılmıştır.
- “data.Each(func(i int, s *goquery.Selection) {“: Herbir filitrelenen data, tek tek gezilmektedir.
- “title := s.Find(“a”).Text() url, _ := s.Find(“a”).Attr(“href”) articleList[title] = url”: Herbir makalenin url ve title’ı bulunarak, articleList’e eklenmiştir.
- “return articleList”: En son da doldurulan liste, geri dönülmektedir.
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 |
package parser import ( "github.com/PuerkitoBio/goquery" "log" "net/http" "time" ) var articleList map[string]string func ParseWeb() map[string]string { client := &http.Client{ Timeout: 30 * time.Second, } res, err := client.Get("https://www.borakasmer.com") if err != nil { log.Fatal(err) } defer res.Body.Close() if res.StatusCode == 200 { doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { log.Fatal(err) } else { data := doc.Find(".entry-title") articleList = make(map[string]string, data.Length()) data.Each(func(i int, s *goquery.Selection) { title := s.Find("a").Text() url, _ := s.Find("a").Attr("href") articleList[title] = url }) return articleList } } else { log.Fatalf("status code error: %d %s", res.StatusCode, res.Status) } return articleList } |
consumer.go: Amaç RabbitMQ’da “company” channel’ı dinlenip kayıt geldiği zaman birden fazla işin tetiklenerek çalıştırılmasıdır. İşler, ilgili kaydın SQLDB’ye kaydedilmesi, tüm kaydın listelenmesi, “borakasmer.com”‘un parse edilip son 10 makalesinin ekrana basılmasıdır.
- “conn, err := amqp.Dial(shared.Config.AMQPURL)”: RabbitMQ’ya ait connection, shared package’dan çekilir.
- “amqpChannel, err := conn.Channel()”: İlgili connection’a ait channel, amqpChannel’a atanır.
- “defer amqpChannel.Close()”: İlgili channel func’ın sonunda, tüm işlemler tamamlanınca kapanır.
- “queue, err := amqpChannel.QueueDeclare(“company”, false, false, false, false, nil)”: “Company” channel’ını dinleyen Queue, tanımlanır.
- “messageChannel, err := amqpChannel.Consume( queue.Name …” : İlgili queue dinlenir ve bir kayıt channel’a düştüğünde, bu messageChannel’a atanır.
- “stopChan := make(chan bool)” İlgili Company Channel’ın conccurent olarak dinlenebilmesi için gerekli olan channeldır.
- “go func() {.. for d := range messageChannel {” : go komutu ile messagChannel, arkada concurrent olarak dinlenir. Go’nun en büyük gücü, Threadler’de minimum kaynak tüketimidir.
- “addCompany := &shared.AddCompany{}”: Queue’den çekilecek Company model, boş olarak shared kütüphanesinden yaratılır.
- “err := json.Unmarshal(d.Body, addCompany)”: .Net Core WebApi ile Queue’ya atılan Company model, deserialize (Unmarshal) edilip addCompany model’e atanmıştır.
- “res, err2 := sql2.InsertSqlContent(db, addCompany)”: Yukarıda tanımlanan sql package’daki Insert function’ı, çağrılıp kuyruğa alınan yeni Company, DB’ye kaydedilir.
- “companyName, companyCode, companyUrl, apiUrl, err := sql2.GetSqlContent(db)”: Son kaydedilen Company kaydı dahil, tüm kayıtlar sql package’daki GetSqlContent() function’ı çağrılarak listelenir.
- “articleList := parser.ParseWeb()” : Son olarak, yukarıda tanımlanan parser package’ındaki ParserWeb() function’ı çağrılarak, “borakasmer.com” portalı parse edilir. Blogdaki son 10 makale alınarak, articleList’e atanır.
- “for title,url :=range articleList{ fmt.Println(“Blog:”, title, “=>”, “Url:”, url) }”: Çekilen tüm makalelerin Title ve Url’leri ekrana listelenir.
- “<-stopChan”: İlgili channel’dan data okunarak, sonlandırılı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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
package rabbitMQ import ( "Test/parser" "Test/shared" sql2 "Test/sql" "database/sql" "encoding/json" "fmt" "github.com/streadway/amqp" "log" "os" "strconv" "strings" ) func Consumer(db *sql.DB) { conn, err := amqp.Dial(shared.Config.AMQPURL) handleError(err, "Can't connect to AMQP") defer conn.Close() amqpChannel, err := conn.Channel() handleError(err, "Can't create a amqpChannel") defer amqpChannel.Close() queue, err := amqpChannel.QueueDeclare("company", false, false, false, false, nil) handleError(err, "Could not declare `add` queue") err = amqpChannel.Qos(1, 0, false) handleError(err, "Could not configure QoS") messageChannel, err := amqpChannel.Consume( queue.Name, "", false, false, false, false, nil, ) handleError(err, "Could not register consumer") stopChan := make(chan bool) go func() { log.Printf("Consumer ready, PID: %d", os.Getpid()) for d := range messageChannel { fmt.Println(strings.Repeat("-", 100)) log.Printf("Received a message: %s", d.Body) addCompany := &shared.AddCompany{} err := json.Unmarshal(d.Body, addCompany) if err != nil { log.Printf("Error decoding JSON: %s", err) } fmt.Println(strings.Repeat("-", 100)) fmt.Printf("Company :%s - ApiUrl: %s\n", addCompany.CompanyName, addCompany.ApiUrl) log.Printf("Company %s of %f. ApiUrl : %s", addCompany.CompanyName, addCompany.CompanyCode, addCompany.ApiUrl) res, err2 := sql2.InsertSqlContent(db, addCompany) handleError(err2, "Could not Insert Product to Sql") log.Printf("Inserted Product ID : %d", res) if err := d.Ack(false); err != nil { log.Printf("Error acknowledging message : %s", err) } else { log.Printf("Acknowledged message") } // SQL List All Product companyName, companyCode, companyUrl, apiUrl, err := sql2.GetSqlContent(db) if err != nil { fmt.Println("(sqltest) Error getting content: " + err.Error()) } fmt.Println(strings.Repeat("-", 100)) // Now read the contents for i := range companyName { fmt.Println("Company Name " + strconv.Itoa(i) + ": " + companyName[i] + ", Company Code: " + strconv.FormatFloat(companyCode[i] , 'f', 2, 64) + ", Company url: " + companyUrl[i] + ", Api Url: " + apiUrl[i]) } //Parse borakasmer.com articleList := parser.ParseWeb() for title,url :=range articleList{ fmt.Println("Blog:", title, "=>", "Url:", url) } } }() // Stop for program termination <-stopChan } func handleError(err error, msg string) { if err != nil { log.Fatalf("%s: %s", msg, err) } } |
Kendi sorununa kendin bizzat sahip çıkmazsan o sorun gün geldiğinde kendiliğinden çözülse bile sen o çözümü hak etmemiş olacaksın. ―Mehmet Murat İldan
main.go: Projenin başlangıç sayfasıdır. İlk proje ayağa kaldırıldığında, main() function’ı çağrılmaktadır.
- “companyNames, companyCodes, companyUrls, apiUrls, err := sql.GetSqlContent(db)”: Öncelikle SQLDB’deki kayıtlı tüm company datası, çekilir.
- “for i := range companyNames {“: Herbir kayıt, tek tek gezilerek ekrana basılır.
- “rabbitMQ.Consumer(db)”: RabbitMQ’daki company channel’ı, dinlenmeye başlanı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 |
package main import ( rabbitMQ "Test/RabbitMQ" "Test/sql" "fmt" "strconv" "strings" ) func main() { var db = sql.SqlOpen() defer db.Close() //GetSqlContent companyNames, companyCodes, companyUrls, apiUrls, err := sql.GetSqlContent(db) if err != nil { fmt.Println("(sqltest) Error getting content: " + err.Error()) } fmt.Println(strings.Repeat("-", 100)) // Now read the contents for i := range companyNames { fmt.Println("Company " + strconv.Itoa(i) + ": " + companyNames[i] + ", CompanyCode: " + strconv.FormatFloat(companyCodes[i], 'f', 2, 64) + ", CopmanyUrl: " + companyUrls[i] + ", ApiUrl: " + apiUrls[i]) } //RabbitMQ Consumer rabbitMQ.Consumer(db) } |
Geldik bir makalenin daha sonuna. Bu makalede, farklı teknolojilerin birbirleri ile nasıl uyumlu çalışabildiklerini hep birlikte inceledik. Bana göre .Net Core WebApi projelerinde hem çok güçlü hem de çok performanslı. Go da Microservices çözümlerinde, Concurrent yapılardaki performansı, minimum kaynak tüketimi ile, maximum iş yapması seçimlerimde büyük rol oynamaktadır. Birden fazla işlemin yapıldığı concurrent yapılarda, Go tam bir performans ve hız canavarıdır. Ayrıca .Net Core katmanlı mimaride ve Repository Pattern’in kullanılmında, kendine özgü bir pratikliğe ve kolaylığa sahiptir.
Kısacası bir teknoloji üzerinde fanatiklik yapmaktansa, farklı teknolojilerin hangi ihtiyaçlara göre yaratıldığına ve bunlardan hangisi veya hangilerinin var olan sorunlarımıza çözüm olabileceğine karar verip, bu şekilde ilerlenmelidir.
Yeni bir makalede görüşmek üzere, hepinize hoşçakalın.
Soruce Code (GO): https://github.com/borakasmer/GoParser/tree/main/Test
Soruce Code (.Net Core): https://github.com/borakasmer/CompanyService/tree/main/CompanyService/CompanyService
Source: github.com/masnun/gopher-and-rabbit, jenicaandpatrick.com, bsilverstrim.blogspot.com, github.com/PuerkitoBio/goquery
Elinize sağlık
Teşekkürler Fehmi..