- Loop cut -> Edit Mode -> CTRL + R
- Extrude -> Edit Mode -> E
- Scale -> Edit Mode -> S
- Tüm nesneler seçilip sonrasında Invidual Origis yapılarak kendi origine göre editlenebilir.
- Select opposite edges, click Alt + E, extrude along normals
Friday, December 12, 2025
Blender Notlar
Sunday, November 30, 2025
Sample DB installation on Oracle DB
$> sqlplus
Enter username: system (or whatever) and the password;
Firstly, set the session container as pdb:
SQL> SHOW PDBS;
SP2-0382: The SHOW PDBS command is not available
SQL> SELECT name, open_mode FROM v$pdbs;
NAME
--------------------------------------------------------------------------------
OPEN_MODE
----------
PDB$SEED
READ ONLY
ORCLPDB
READ WRITE
SQL> ALTER SESSION SET CONTAINER=ORCLPDB;
Session altered.
SQL> SHOW CON_NAME;
CON_NAME
------------------------------
ORCLPDB
SQL> @sh_install.sql
Thank you for installing the Oracle Sales History Sample Schema.
This installation script will automatically exit your database session
at the end of the installation or if any error is encountered.
The entire installation will be logged into the 'sh_install.log' log file.
Enter a password for the user SH:
Enter a tablespace for SH [USERS]: USERS
Do you want to overwrite the schema, if it already exists? [YES|no]:
CREATE USER sh IDENTIFIED BY "oracle"
*
ERROR at line 1:
ORA-65096: invalid common user or role name
Disconnected from Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
Version 19.3.0.0.0
That's why we need to create sh user.
SQL> CREATE USER sh IDENTIFIED BY oracle
2 DEFAULT TABLESPACE users
3 TEMPORARY TABLESPACE temp
4 QUOTA UNLIMITED ON users;
User created.
SQL> GRANT CONNECT, RESOURCE, CREATE VIEW, CREATE PROCEDURE TO sh;
Grant succeeded.
SQL> GRANT UNLIMITED TABLESPACE TO sh;
Grant succeeded.
Let's try to install again:
SQL> @sh_install.sql
Thank you for installing the Oracle Sales History Sample Schema.
This installation script will automatically exit your database session
at the end of the installation or if any error is encountered.
The entire installation will be logged into the 'sh_install.log' log file.
Enter a password for the user SH:
Enter a tablespace for SH [USERS]: USERS
Do you want to overwrite the schema, if it already exists? [YES|no]: YES
Old SH schema has been dropped.
Start time: 30-NOV-25 04.23.31.746000 PM +03:00
****** Creating COUNTRIES table ....
Table created.
****** Creating CUSTOMERS table ....
Table created.
****** Creating PROMOTIONS table ....
Table created.
****** Creating PRODUCTS table ....
Table created.
****** Creating TIMES table ....
Table created.
****** Creating CHANNELS table ....
Table created.
****** Creating SALES table ....
Table created.
****** Creating COSTS table ....
Table created.
****** Creating SUPPLEMENTAL_DEMOGRAPHICS table ....
Table created.
****** Creating views ....
View created.
****** Creating materialized views ....
Materialized view created.
Materialized view created.
****** Adding comments to tables...
Comment created.
...
Comment created.
Session altered.
****** Disabling table constraints for the load
Table altered.
...
Table altered.
****** Populating CHANNELS table ....
PL/SQL procedure successfully completed.
****** Populating COUNTIRES table ....
PL/SQL procedure successfully completed.
****** Populating PRODUCTS table ....
PL/SQL procedure successfully completed.
Commit complete.
SP2-0158: unknown SET option "LOAD"
****** Populating COSTS table ....
SP2-0734: unknown command beginning "LOAD costs..." - rest of line ignored.
****** Populating CUSTOMERS table ....
SP2-0734: unknown command beginning "LOAD custo..." - rest of line ignored.
****** Populating PROMOTIONS table ....
SP2-0734: unknown command beginning "LOAD promo..." - rest of line ignored.
****** Populating SALES table ....
SP2-0734: unknown command beginning "LOAD sales..." - rest of line ignored.
****** Populating TIMES table ....
SP2-0734: unknown command beginning "LOAD times..." - rest of line ignored.
****** Populating SUPPLEMENTARY_DEMOGRAPHICS table ....
SP2-0734: unknown command beginning "LOAD suppl..." - rest of line ignored.
****** Enabling table constraints
Table altered.
...
Table altered.
****** Creating indexes for SALES table ....
Index created.
...
Index created.
****** Creating indexes for COSTS table ....
Index created.
Index created.
****** Creating indexes for PRODUCTS table ....
Index created.
...
Index created.
****** Creating dimensions ....
Dimension created.
...
Dimension created.
Dimension created.
****** Gathering statistics for schema ...
PL/SQL procedure successfully completed.
End time: 30-NOV-25 04.23.37.822000 PM +03:00
1 row selected.
Installation
-------------
Verification:
Table provided actual
-------------------------- ---------- ----------
channels 5 5
costs 82112 0
countries 35 35
customers 55500 0
products 72 72
promotions 503 0
sales 918843 0
times 1826 0
supplementary_demographics 4500 0
Thank you!
--------------------------------------------------------
The installation of the sample schema is now finished.
Please check the installation verification output above.
You will now be disconnected from the database.
Thank you for using Oracle Database!
Disconnected from Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
Version 19.3.0.0.0
Thursday, June 27, 2024
Godot'da Avalonia ile UI Oluşturma
dotnet add package JLeb.Estragonia --version 1.1.1
Bu komutu kullanarak paketi yükleyebiliriz. .csproj uzantılı dosyayı kontrol ettiğimde paketin yüklendiğini anlıyorum:
<Project Sdk="Godot.NET.Sdk/4.2.2">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net7.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JLeb.Estragonia" Version="1.1.1" />
</ItemGroup>
</Project>
Ayrıca Avalonia Nuget theme paketini yüklememiz gerekiyor. Projenin geliştiricisi bunun sebebini, diğer türlü Avalonia'dan görüntü alamayız olarak belirtmiş.
dotnet add package Avalonia.Themes.Fluent --version 11.0.11
Gerekli paketleri yükledikten sonra UserInterface.cs script'ine dönelim ve sınıfın miras aldığı sınıfı JLeb.Estragonia.AvaloniaControl olarak değiştirelim. _Ready ve _Process metotları aşağıdaki gibi olsun:
using Godot;
using System;
public partial class UserInterface : JLeb.Estragonia.AvaloniaControl
{
public override void _Ready()
{
}
public override void _Process(double delta)
{
}
// Called when the node enters the scene tree for the first time.
}
<Application
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="GodotWithAvalonia.App">
<Application.Styles>
<FluentTheme />
</Application.Styles>
<Application>
App.axaml.cs:
using Avalonia;
using Avalonia.Markup.Xaml;
namespace GodotWithAvalonia;
public class App : Application {
public override void Initialize()
=> AvaloniaXamlLoader.Load(this);
}
using Avalonia;
using Godot;
using JLeb.Estragonia;
namespace GodotWithAvalonia;
public partial class AvaloniaLoader : Node {
public override void _Ready()
=> AppBuilder
.Configure<App>()
.UseGodot()
.SetupWithoutStarting();
}
...
[application]
config/name="GodotWithAvalonia"
run/main_scene="res://main_scene.tscn"
config/features=PackedStringArray("4.2", "C#", "Forward Plus")
config/icon="res://icon.svg"
[autoload]
AvaloniaLoader="res://UI/AvaloniaLoader.cs"
[dotnet]
project/assembly_name="GodotWithAvalonia"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="GodotWithAvalonia.TestView">
<TextBlock Text="Merhaba Godot!" />
</UserControl>
TestView.axaml.cs:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace GodotWithAvalonia;
public partial class TestView : UserControl
{
public TestView()
{
InitializeComponent();
}
}
using Godot;
using GodotWithAvalonia;
using System;
public partial class UserInterface : JLeb.Estragonia.AvaloniaControl
{
public override void _Ready()
{
Control = new TestView();
base._Ready();
}
public override void _Process(double delta)
{
base._Process(delta);
}
}
Tuesday, June 25, 2024
SignalR ve WebSockets Transport Kullanımı
SignalR'ın client ve server durumuna göre transport metodlarından en uygun hangisi ise onu seçtiğini biliyoruz. Ancak geliştirme ortamında projemin tam olarak benim istediğim şekilde anlık sonuç vermemesi istediğim bir şey değil. Bu noktadan itibaren önceki yazıyı baz alarak devam edeceğim.
TrackHub sınıfında override ettiğim OnDisconnectedAsync metoduna breakpoint koyduğumda, client kapatıldığında bu metoda düşmesi. Ancak OnDisconnectedAsync metotu tetiklenmiyor. Bu sebeple anlık olarak bir değişme var mı, göremiyorum.
Geliştirme ortamında projemi geliştirirken farkettiğim şey SignalR'ın transport metotu olarak Long Polling kullanmasıydı.
public override async Task OnConnectedAsync()
{
var transportType = Context.Features.Get<IHttpTransportFeature>().TransportType;
...
}
Bu durum beklenen bir şey, ancak beklentim tarayıcım modern bir tarayıcı olduğu için ve muhtemelen asp.net core api projemin çalışması için sunulan IIS Express'de WebSockets transport'u destekliyor diye, WebSockets'i ilk olarak tercih eder demiştim, öyle olmuyormuş.
builder.Services.AddWebSockets(o => {
o.AllowedOrigins.Add("https://localhost:7058");
o.AllowedOrigins.Add("https://localhost:7212");
});
Sadece belli origin'ler için websockets bağlantısı yapılmasını istiyorum o yüzden AddWebSockets ile bu sınırlandırmayı yukarıdaki gibi yapabiliriz. Bu güvenlik açısından önemli bir ayarlama.
Daha sonrasında cors policy'i aşağıdaki gibi ayarladım:
builder.Services.AddCors(options =>
{
options.AddPolicy("all", builder => builder
.WithOrigins("https://localhost:7058", "https://localhost:7212")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials() // Eğer credential (kimlik doğrulama) kullanılıyorsa ekleyin.
.SetIsOriginAllowed((host) => true) // CORS doğrulamasını özelleştirmek için gerekebilir.
);
});
Ayrıca SignalR Hub'ın sadece WebSockets bağlantılarını kabul etmesini istiyorum. Diğer transport'lar devredışı kalmış oluyor:
app.MapHub<TrackHub>("track", o => {
o.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets;
});
Bir önemli nokta ise geliştirme ortamında kullandığım sunucu olan IIS Express'in WebSockets'i desteklemesi gerekli. Bunun için Turn Windows features on or off Windows işletim sistemi arama kısmına yazıp:
- Internet Information Services
- World Wide Web Services
- Application Development Features
- WebSocket Protocol'u seçip aktif hale getirin
protected async override Task OnInitializedAsync()
{
var authState = await authenticationStateTask;
var user = authState.User;
string username = string.Empty;
if (user.Identity.IsAuthenticated)
{
username = user.Identity.Name;
}
string group = "admin";
...
string token = await localStorage.GetItem("token");
hubConnection = new HubConnectionBuilder()
.WithUrl(
"https://localhost:44342/track",
o => {
o.AccessTokenProvider = () => Task.FromResult<string?>(token);
o.Url = new Uri($"https://localhost:44342/track?username={username}<group={group}");
o.SkipNegotiation = true;
o.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets;
}
)
.Build();
...
isLoaded = true;
}
SkipNegotiation true yaparak direkt hangi transport'u seçtiysek doğrudan o transport'u kullanmasını müzakere yapmadan sağlamış oluyoruz. Projeyi daha sonrasında çalıştırırsak muhtemelen alacağımız hata client tarafında şu olacaktır:
WebSocket connection to 'wss://localhost:44342/.. Authentication failed; no valid credentials available
Bu hatanın çözümünü bulamamıştım ta ki dünyanın en iyi arama motoru ChatGPT gerekli çözümü verene kadar:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.SaveToken = true;
o.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
ValidIssuer = configuration["JwtSettings:Issuer"],
ValidAudience = configuration["JwtSettings:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JwtSettings:Key"]))
};
o.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/track")))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
OnMessageReceived event'i her HTTP mesajı için tetiklenir, amaç JWT token gönderen durumları dinlemektir. accessToken boş değilse ve url track ile başlıyorsa context.Token'a atama token ataması gerçekleştirilir. Yani kısacası OnMessageReceived token'i alır, token doğrulanır ve 'Authorize' attribute'u eğer token doğrulandıysa erişimi sağlar.
Thursday, June 20, 2024
SignalR ve Garnet ile Anlık Kullanıcıları İzleme
...
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSignalR();
var app = builder.Build();
...
Hub temelinde bir sınıf oluşturmamız lazım. Sınıfın ismi TrackHub olsun. Bu Hub sınıfı bir API olarak client'lar ile server arasında iletişim kurmamızı sağlayacak. Identity katmanında Hubs klasörü oluşturdum. Bu klasörde hub sınıflarım yer alacak. TrackHub.cs dosyası olacak şekilde ilk sınıfımı ekledim:using Application.Contracts.Hubs;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace Identity.Hubs
{
[Authorize]
public class TrackHub : Hub<ITrackHub>
{
public override async Task OnConnectedAsync()
{
var httpContext = Context.GetHttpContext();
if (httpContext != null)
{
var userName = httpContext.Request.Query["username"];
}
await base.OnConnectedAsync();
}
}
}
Authorize ile yetki gerekliliği sağladık bu sınıf için, sonuçta herkes bu hub'a bağlanamaz. Aynı zamanda Hub sınıfının kendisi strongly typed olmadığı için kendi strongly typed Hub'ımızı oluşturmak için bize interface kullanma imkanı sağlıyor. Bu sayede mesaj gönderirken hangi parametrelerin kullanılacağına dair bir kural oluşturmuş oluyoruz. Diğer türlü belli bir kurala bağlı olmayacağımız için eğer hata yaparsak mesaj gönderirken bunu runtime esnasında anlayabilmemiz mümkün oluyor.
namespace Application.Contracts.Hubs
{
public interface ITrackHub
{
Task ReceiveActiveUserCounter(int counter);
}
}
httpContext'i ise hangi kullanıcının hub'a bağlandığını anlamak amacıyla kullandım. Yazının devamında blazor wasm tarafında hub'a bağlanırken username'i parametre olarak göndereceğiz.
Ayrıca bir tane TrackBackgroundService adında bir Background Service oluşturdum. IHostedService olan background servisimiz arkaplanda concurrent olarak bizim belirlediğimiz süreye göre SignalR kullanarak mesaj gönderecek. Ama burada şöyle bir durum olacak. Eğer kullanıcı yönetim panelinden girdiyse bu kullanıcı veya kullanıcılara mesaj gönderilecek. Mesaj olarak göndereceğimiz şey ise Blazor Wasm uygulamasından giriş yapan kullanıcı bilgileri olacak. Background Service sayesinde anlık bir kontrol yapmış olacağız. Ancak background service kullanmamız gerekmiyor olabilir bu senaryoda ama nasıl kullanılacağını görmek amacıyla güzel bir pratik olur dedim.
using Application.Contracts.Hubs;
using Identity.Hubs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
namespace Identity.Services.Hubs
{
public class TrackBackgroundService : BackgroundService
{
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(10);
private readonly IHubContext<TrackHub, ITrackHub> _context;
public TrackBackgroundService(IHubContext<TrackHub, ITrackHub> context)
{
_context = context;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(Timeout);
while (!stoppingToken.IsCancellationRequested &&
await timer.WaitForNextTickAsync(stoppingToken))
{
//await _context.Clients.Client(userService.GetUserId()).ReceiveActiveUserCounter(12);
// mesaj gönder
}
}
}
}
Hosted service olduğu için kayıt işlemini aşağıdaki gibi yapıyoruz:
});
services.AddHostedService<TrackBackgroundService>();
return services;
}
}
Oluşturduğumuz TrackHub'a gelecek olan request'leri alabilmek için kullanacağımız endpoint'i Program.cs dosyasında aşağıdaki gibi bildiriyorum:
app.UseHttpsRedirection();
app.MapHub<TrackHub>("track");
app.UseCors("all");
Tabi production'da CORS "all" falan diye kalmasın kendinize göre ayarlayın.namespace Application.Contracts.Identity
{
public interface IGarnetCacheService
{
Task<string> GetValueAsync(string key);
Task<bool> SetValueAsync(string key, string value);
Task AppendListAsync(string key, string element);
Task<bool> RemoveFromList(string key, string element);
Task Clear(string key);
void ClearAll();
}
}
Bu interface'in implementasyonunu yazmadan önce bir Garnet'in kurulumunu yapın. Aslında tam olarak kurulum denemez, sadece bir executable file. microsoft/garnet Buradan repo clone alıp kendiniz de build edebilirsiniz veya yine aynı linkten Garnet'in son release sürümünü indirebilirsiniz. İndirdikten sonra sadece garnet-server dosyasını çalıştırmanız yeterli. Sonrasında ekrana şöyle bir şey gelecek: "AllowedHosts": "*",
"Garnet": {
"Address": "127.0.0.1",
"Port": 6379,
"UseTLS": false
}
}
Sonrasında API tarafında bu değerleri alabilmek için uygun bir DTO oluşturdum infrastructure katmanında:
namespace Identity.Models.DTO
{
public class GarnetConfiguration
{
public int Port { get; set; }
public string Address { get; set; } = string.Empty;
public bool UseTLS { get; set; }
}
}
Service kayıt sınıfında aşağıdaki configurasyonu ve gerekli IGarnetCacheService'i singleton service olarak ekledim:
services.Configure<GarnetConfiguration>(configuration.GetSection("Garnet"));
services.AddSingleton<IGarnetCacheService, GarnetCacheService>();
GarnetCacheService'i GarnetClient'a göre ve oluşturduğumuz interface'e uygun şekilde implement edelim:
using Application.Contracts.Identity;
using Garnet.client;
using Identity.Models.DTO;
using Microsoft.Extensions.Options;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace Identity.Services
{
public class GarnetCacheService : IGarnetCacheService
{
private readonly GarnetConfiguration _g;
public GarnetCacheService(IOptions<GarnetConfiguration> garnetConfiguration)
{
_g = garnetConfiguration.Value;
}
public async Task Clear(string key)
{
using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());
await db.ConnectAsync();
if (await Contains(db, key))
{
await db.KeyDeleteAsync(key);
}
}
public async void ClearAll()
{
using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());
await db.ConnectAsync();
await db.ExecuteForStringResultAsync("FLUSHDB");
}
public async Task<string> GetValueAsync(string key)
{
using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());
await db.ConnectAsync();
return await db.StringGetAsync(key);
}
public async Task<bool> SetValueAsync(string key, string value)
{
using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());
await db.ConnectAsync();
await db.StringSetAsync(key, value);
string check = await db.StringGetAsync(key);
if (check == value)
{
return true;
}
return false;
}
public async Task AppendListAsync(string key, string element)
{
using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());
await db.ConnectAsync();
await db.ListRightPushAsync(key, element);
}
public async Task<bool> RemoveFromList(string key, string element)
{
using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());
await db.ConnectAsync();
bool isRemove = int.Parse(await db.ExecuteForStringResultAsync("LREM", new string[] { key, "0", element })) == 1 ? true : false;
return isRemove;
}
private async Task<bool> Contains(GarnetClient db, string key)
{
bool exist = int.Parse(await db.ExecuteForStringResultAsync("EXISTS", new string[] { key })) == 1 ? true : false;
return exist;
}
SslClientAuthenticationOptions GetSslOpts() => _g.UseTLS ? new()
{
ClientCertificates = [new X509Certificate2("certfile.pfx", "temp")],
TargetHost = "GarnetTest",
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,
} : null;
}
}
Yaptığım implementasyon çalışacak mı emin olmamakla beraber çalışacağını umuyorum. Bu arada Garnet kullanımı çok muhtemel kötü bir kullanım yöntemi o yüzden sonrasında iyileştirme yapmak gerekecektir.Garnet yeni olduğu için ve ben de ilk defa kullandığım için bu şekilde yazdım daha iyi kavramak amacıyla.
using Application.Contracts.Hubs;
using Application.Contracts.Identity;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace Identity.Hubs
{
[Authorize]
public class TrackHub : Hub<ITrackHub>
{
private readonly IGarnetCacheService _cache;
public TrackHub(IGarnetCacheService cache)
{
_cache = cache;
}
public override async Task OnConnectedAsync()
{
var httpContext = Context.GetHttpContext();
if (httpContext != null)
{
string connectionId = Context.ConnectionId;
string? userName = httpContext.Request.Query["username"];
if(userName != null)
{
await _cache.AppendListAsync("activeUsers", userName);
}
}
await base.OnConnectedAsync();
}
}
}
Şimdi sıra SignalR Hub'ımız düzgün çalışıyor mu ve kullanıcı adı Garnet Server'a ekleniyor mu test etmeye geldi. İlk olarak Blazor Wasm uygulaması olan yönetim paneli projesine öncelikle Microsoft.AspNetCore.SignalR.Client(8.0.6) paketini yükledim. Bu paket sayesinde HubConnection oluşturabileceğim client ile server arasında. HubConnection'ı sağlamak için Dashboard.razor'u seçtim. Bu seçim tamamen size bağlı istediğiniz yerde connection'u sağlayabilirsiniz:
@using AdminUI.Models.Stat
@using Microsoft.AspNetCore.SignalR.Client
@implements IAsyncDisposable
...
@code{
private HubConnection? hubConnection;
[Inject]
LocalStorageService localStorage { get; set; }
[Inject]
public IForumStatService forumStatService { get; set; }
...
int activeUserCounter = 0;
bool isLoaded;
[CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; }
protected async override Task OnInitializedAsync()
{
var authState = await authenticationStateTask;
var user = authState.User;
string username = string.Empty;
if (user.Identity.IsAuthenticated)
{
username = user.Identity.Name;
}
...
string token = await localStorage.GetItem("token");
hubConnection = new HubConnectionBuilder()
.WithUrl(
"https://localhost:7147/track",
o => {
o.AccessTokenProvider = () => Task.FromResult<string?>(token);
o.Url = new Uri($"https://localhost:7147/track?username={username}");
}
)
.Build();
hubConnection.On<int>("ReceiveActiveUserCounter", async message =>
{
activeUserCounter = message;
await InvokeAsync(StateHasChanged);
});
await hubConnection.StartAsync();
isLoaded = true;
}
public async ValueTask DisposeAsync()
{
if(hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
}
API tarafında çalışan TrackHub ile bağlantıyı yetki almak üzere token ile sağlıyorum. Server tarafında ReceiveActiveUserCounter mesajı gönderildikçe client'da ReceiveActiveUserCounter içindeki kod bloğu çalıştırılmış olacak. Dikkat ederseniz Client burada sadece bağlanmak için Hub'a bir kez istek attı sonrasında Server'dan client içindeki metot request olmadan çalıştırılabilecek. public class GarnetCacheService : IGarnetCacheService
{
private readonly GarnetConfiguration _g;
public GarnetCacheService(IOptions<GarnetConfiguration> garnetConfiguration)
{
_g = garnetConfiguration.Value;
}
...
public async Task AddHashSet(string key, string field, string value)
{
using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());
await db.ConnectAsync();
await db.ExecuteForStringResultAsync("HSET", new string[] { key, field, value });
}
HSET command'i key adında bir hashset yoksa oluşturmamızı ve ayrıca içerisinde field ve ona karşılık value'yi store etmemizi sağlıyor. Eğer önceden field mevcut ise sadece value değerini güncelliyor.
public override async Task OnConnectedAsync()
{
var httpContext = Context.GetHttpContext();
if (httpContext != null)
{
string connectionId = Context.ConnectionId;
string? userName = httpContext.Request.Query["username"];
if(userName != null)
{
await _cache.AddHashSet("activeUsers", userName, connectionId);
}
}
await base.OnConnectedAsync();
}
Şimdi ilk hub bağlantı sonrası redis-cli üzerinden kontrol edelim:
Bir sorun görülmüyor, tekrar hub bağlantısını yenileyelim:Tam olarak istediğimiz sonucu elde etmiş olduk. Sadece kullanıcı adını kontrol ederek varsa eğer son ConnectionId ile güncellemiş olduk. public class ActiveUser
{
public string UserName { get; set; } = string.Empty;
public string? ConnectionId { get; set; }
}
Sonrasında ITrackHub'a ReceiveActiveUsers signature'u ekledim:
public interface ITrackHub
{
Task ReceiveActiveUserCounter(int counter);
Task ReceiveActiveUsers(string jsonStr);
}
Test amaçlı BackgroundService'i aşağıda gibi güncelledim:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(Timeout);
while (!stoppingToken.IsCancellationRequested &&
await timer.WaitForNextTickAsync(stoppingToken))
{
var exampleList = new List<ActiveUser>() { new ActiveUser { UserName = "test", ConnectionId = "testcon" } };
var jsonStr = JsonConvert.SerializeObject(exampleList);
await _context.Clients.All.ReceiveActiveUsers(jsonStr);
}
}
Blazor WASM uygulaması tarafında yine aynı şekilde ActiveUserVM adı ile aynı sınıfı oluşturuyorum:
@using AdminUI.Models.Hub
@using AdminUI.Models.Stat
@using Microsoft.AspNetCore.SignalR.Client
@using System.Text.Json.Serialization
@using System.Text.Json
@implements IAsyncDisposable
...
@code{
private HubConnection? hubConnection;
[Inject]
LocalStorageService localStorage { get; set; }
...
int activeUserCounter = 0;
private List<ActiveUserVM>? activeUsers;
bool isLoaded;
[CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; }
protected async override Task OnInitializedAsync()
{
var authState = await authenticationStateTask;
var user = authState.User;
string username = string.Empty;
if (user.Identity.IsAuthenticated)
{
username = user.Identity.Name;
}
...
hubConnection.On<string>("ReceiveActiveUsers", async message =>
{
if(message != null)
{
activeUsers = JsonSerializer.Deserialize<List<ActiveUserVM>>(message);
}
await InvokeAsync(StateHasChanged);
});
await hubConnection.StartAsync();
isLoaded = true;
}
...
}
Ayrıca bu kullanıcı listesini arayüzde görmek için bir liste arayüzü oluşturalım geçici olarak:
@using AdminUI.Models.Hub
@using AdminUI.Models.Stat
@using Microsoft.AspNetCore.SignalR.Client
@using System.Text.Json.Serialization
@using System.Text.Json
@implements IAsyncDisposable
...
<div class="row">
@if(activeUsers != null)
{
<ul>
@foreach(var activeUser in activeUsers){
<li>@activeUser.UserName - @activeUser.ConnectionId</li>
}
</ul>
}
</div>
...
@code{
...
}
Şimdi projeyi çalıştıralım ve bakalım client tarafına mesajımız başarılı bir şekilde geliyor mu kontrol edelim:
public interface IGarnetCacheService
{
...
Task<Dictionary<string, string>?> GetAllHashSet(string key);
...
}
Implemantasyonunu aşağıdaki gibi HGETALL command'i kullanarak yaptım.
public async Task<Dictionary<string, string>?> GetAllHashSet (string key)
{
using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());
await db.ConnectAsync();
string[] hashArray = await db.ExecuteForStringArrayResultAsync("HGETALL", new string[] { key });
if(hashArray.Length > 0)
{
Dictionary<string, string> hash = new Dictionary<string, string>();
for (int i = 0; i < hashArray.Length; i+=2)
{
hash.Add(hashArray[i], hashArray[(i + 1)]);
}
return hash;
}
return null;
}
Daha sonrasında Background Service'da bunu direkt mesaj olarak client'a gönderebiliriz. Ama biz şimdilik onu List<ActiveUser> yapısında gönderelim:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(Timeout);
while (!stoppingToken.IsCancellationRequested &&
await timer.WaitForNextTickAsync(stoppingToken))
{
List<ActiveUser> activeUsers = new List<ActiveUser>();
Dictionary<string, string>? activeUserHashSet = await _cache.GetAllHashSet("activeUsers");
if(activeUserHashSet != null)
{
foreach (var activeUser in activeUserHashSet)
{
activeUsers.Add(new ActiveUser { ConnectionId = activeUser.Value, UserName = activeUser.Key });
}
}
var jsonStr = JsonConvert.SerializeObject(activeUsers);
await _context.Clients.All.ReceiveActiveUsers(jsonStr);
}
}
Background servisi güncelledikten sonra tekrar uygulamamızı çalıştıralım:
using BlazorUI.Contracts;
using Microsoft.AspNetCore.Components;
using BlazorUI.Models.Authentication;
using Microsoft.AspNetCore.SignalR.Client;
using BlazorUI.Services.Common;
namespace BlazorUI.Pages;
public partial class Login
{
public LoginVM Model { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
public string Message { get; set; }
[Inject]
private IAuthenticationService AuthenticationService { get; set; }
private HubConnection? hubConnection;
[Inject]
LocalStorageService localStorage { get; set; }
public Login()
{
}
...
protected async Task HandleLogin()
{
if (await AuthenticationService.AuthenticateAsync(Model.Email, Model.Password))
{
NavigationManager.NavigateTo("/");
string token = await localStorage.GetItem("token");
hubConnection = new HubConnectionBuilder()
.WithUrl(
"https://localhost:7147/track",
o => {
o.AccessTokenProvider = () => Task.FromResult<string?>(token);
o.Url = new Uri($"https://localhost:7147/track?username={Model.Email}");
}
)
.Build();
await hubConnection.StartAsync();
}
Message = "Username/password combination unknown";
}
}
İlk olarak API ve Admin uygulamasını çalıştıracağım daha sonrasında kullanıcı uygulamasını çalıştıracağım, bu sayede aktif kullanıcılara ekleniyor mu görmüş olacağız:
Başarılı bir şekilde diğer kullanıcı yönetim panelinde aktif kullanıcı listesinde görünür oldu. Yazıyı daha fazla uzatmamak adına burada sonlandırıyorum. Muhtemel gözümden kaçan hatalar, yazım yanlışları olabilir. Tuesday, June 18, 2024
.NET Günlük #1 - Late Binding, Early Binding, const, readonly
C# üzerine yazacağım notları bu blog üzerinde toplamaya karar verdim. Hem teorik anlam da hem de pratik olarak küçük notlar barındıracak bir yazı serisi olacak. Merak ettiğim veya öğrenmek istediğim konular hakkında araştırmalardan öğrendiklerimi bu yazı serisine ekleyeceğim. Tamamen birbirinden bağımsız konular bir yazı içeriğinde toplanabilir. Kesinlikle bir tutorial serisi amacı taşımamaktadır.
Early Binding ve Late Binding
const ve readonly
Friday, November 3, 2023
WPF, MVVM (CommunityTookit.Mvvm) and Dependency Injection
- Models
- ViewModels
- Views
<UserControl x:Class="LDG_WPF.Views.TestView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:LDG_WPF.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Width="200" MaxLength="100">
</TextBox>
<Button Grid.Column="1" Content="Export" />
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="2*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"></TextBlock>
</Grid>
</Grid>
</UserControl>
public partial class TestViewModel: ObservableObject
{
[ObservableProperty]
string? displayText;
[ObservableProperty]
string? inputText;
[RelayCommand]
public void SetText()
{
DisplayText = InputText;
}
}
<UserControl x:Class="LDG_WPF.Views.TestView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewmodels="clr-namespace:LDG_WPF.ViewModels"
xmlns:local="clr-namespace:LDG_WPF.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.DataContext>
<viewmodels:TestViewModel x:Name="test"/>
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Width="200" MaxLength="100" Text="{Binding InputText}">
</TextBox>
<Button Grid.Column="1" Content="Export" Command="{Binding SetTextCommand}" />
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="2*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding DisplayText}"></TextBlock>
</Grid>
</Grid>
</UserControl>
<Window x:Class="LDG_WPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:LDG_WPF"
xmlns:views="clr-namespace:LDG_WPF.Views"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<views:TestView/>
</Grid>
</Window>using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Configuration;
using System.Data;
using System.Windows;
namespace LDG_WPF
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public IServiceProvider ServiceProvider { get; private set; }
public IConfiguration Configuration { get; private set; }
protected override void OnStartup(StartupEventArgs e)
{
var builder = new ConfigurationBuilder();
Configuration = builder.Build();
var serviceCollection = new ServiceCollection();
ConfigureServices(serviceCollection);
ServiceProvider = serviceCollection.BuildServiceProvider();
var mainWindow = ServiceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
private void ConfigureServices(IServiceCollection services)
{
services.AddTransient(typeof(MainWindow));
}
}
}<Application x:Class="LDG_WPF.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:LDG_WPF"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>







