Blazor’da FluentValidation
Selamlar,
Bu makalede, .Net Core üzerinde populer olarak kullanılan FluentValidation‘ın Blazor ile beraber nasıl kullanılabileceğini hep beraber inceleyeceğiz. Blazor hakkında herhangi bir fikriniz yok ise, öncelikle bu link‘i sonra şu link‘i inceleyebilirsiniz. Bildiğiniz üzere, .Net Core üzerinde hali hazırda gelen ve hata ayıklama amaçlı kullanılan [DataAnnotation]’lar bulunmaktadır. FluentValidationlar da, bu hata kontrollerinin daha esnek hale getirilebilmesini, kolayca değiştirilebilmesini ve genişletilebilmesine olanak sağlamaktadır. Kullanım şekilleri Angular’ın, Reactive Form Validationlarına benzemektedir.
Bu örnekte amaç, bir motorsiklet servisinde müşteri kaydı girilmektir. Müşteri kaydı girilirken, müşteriye ait motor(bike) kaydı ve yine müşteriye ait servis(service) kayıtları da beraberinde girilmektedir. Bu giriş işlemi sırasında, belli alanlar FluentValidation kullanılarak kontrol edilmektedir.
Öncelikle gelin, .Net Core’da bir Blazor projesi oluşturalım. Bu makalenin yazıldığı an itibari ile kullanılan en son .Net Core 3.0 sürümü aşağıdaki gibidir.
Not: En son versiyon .Net Core 3.0 kütüphanesi ile çalışmaya dikkat edin.
1 |
dotnet new -h |
Eğer yukarıdaki komut yazıldığında, aşağıdaki blazor templateler gelmiyor ise, öncelike .Net Core CLI’a aşağıdaki komut ile, ilgili Blazor Templateler’i yüklenir.
1 |
dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.0.0-preview9.19424.4 |
Şimdi sıra geldi Blazor projesini oluşturmaya. Aşağıdaki komut ile Blazor “bikeservice” projesi oluşturulur. 2 proje template’inden bu makalede, “blazorserver” seçilmiştir. Nedir blazor server, yine javascript yazılmaya ihtiyaç duyulmadan .Net Codelarının yazıldığı ama bu sefer browser’da değil de, server side’da çalışan client side ile server side arasında iletişimin signalR ile yapıldığı yapılardır.
1 |
dotnet new blazorserver -o bikeservice |
Açılan Projeden Pages klasörü altına BikeService.razor aşağıdaki gibi eklenir.
BikeService.razor: Şu an sayfada, sadece boş bir başlık gözükmektedir.
1 2 3 4 5 6 7 8 |
@page "/bikeservice" <h1>Coder Bike Service</h1> <p>Customer Service Records</p> @code { } |
Shared/NavMenu.razor içerisine yönlendirme amaçlı ilgili bikeservice sayfası aşağıdaki gibi eklenir.
1 2 3 4 5 6 7 8 9 10 11 12 |
<div class="@NavMenuCssClass" onclick="@ToggleNavMenu"> <ul class="nav flex-column"> . . . <li class="nav-item px-3"> <NavLink class="nav-link" href="bikeservice"> <span class="oi oi-list-rich" aria-hidden="true"></span> Bike Service </NavLink> </li> </ul> </div> |
Program.cs : İstenen bir porttan sayfanın yayımlanabilmesi için, “webBuilder.UseUrls(“http://localhost:1923″)” satırı aşağıdaki gibi eklenir.
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"); }); |
Sayfanın son hali aşağıdaki gibidir:
Öncelikle gelin classlarımızı aşağıdaki gibi tanımlayalım:
Customer.cs, Bike.cs, Service.cs: Customer sınıfının Name ve Surname kolonları haricinde diğer 2 kolonu, Bike ve Service sınıflarınca temsil edilmektedirler. İlgili kolonlar, Customer’a ait tek bir Bike kolunu ve birden fazla Service sınıfına ait bir listeden oluşmaktadır.
Bike sınıfı, Model, yıl ve SeriNo kolonlarından oluşmaktadır.
Service sınıfı, Type enum tipinde ServiceType, SeriNo ve Price kolonlarından oluşmaktadı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 |
using System.Collections.Generic; namespace bikeservice.Data { public class Customer { public string Name { get; set; } public string SurName { get; set; } public Bike Bike { get; } = new Bike(); public List<Service> Services { get; } = new List<Service>(); } public class Bike { public string Model { get; set; } public string Year { get; set; } public string Serino { get; set; } } public class Service { public enum Type { BinBakım, AltıBinBakım,OnikiBinBakım } public Type ServiceType { get; set; } public string SeriNo { get; set; } public decimal Price { get; set; } } } |
1 |
dotnet add package FluentValidation |
- RuleFor() : İlgili kolon için kontrol amaçlı bir kural atanır.
- NotEmpty(): Seçili kolon boş olamaz.
- MaximumLength(): Seçili kolonun Maximum değeri belirlenir.
- WithMessage() : Kurala uymayan bir durum ile karşılaşıldığında, yazılacak mesajın tanımlandığı methoddur.
- SetValidator() : Kontrol amaçlı, başka bir sınıf belirlenir.
- Matches() : İstenen bir RegEx tanımı, ilgili method içinde yapılır.
- RuleForEach(customer => customer.Services).SetValidator(new ServiceValidator()) : Customer sınıfına ait Services listesinin kontrolu, “ServiceValidator” adında başka bir sınıfa verilmiştir. Her bir servis kayıdı, ServiceValidator sınıfı tarafından tek tek “RuleForEach()” methodu ile kontrol edilmektedir.
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 |
using FluentValidation; namespace bikeservice.Data { public class CustomerValidator : AbstractValidator<Customer> { public CustomerValidator() { RuleFor(customer => customer.Name).NotEmpty().MaximumLength(10); RuleFor(customer => customer.Surname).NotEmpty().MaximumLength(10); RuleFor(customer => customer.Bike).SetValidator(new BikeValidator()); RuleFor(customer => customer.Services).NotEmpty().WithMessage("You have to define at least one service"); RuleForEach(customer => customer.Services).SetValidator(new ServiceValidator()); } } public class BikeValidator : AbstractValidator<Bike> { public BikeValidator() { RuleFor(bike => bike.Model).NotEmpty(); RuleFor(bike => bike.Year).NotEmpty().Matches("^[0-9]*$").WithMessage("Year must be number!"); RuleFor(bike => bike.Serino).NotEmpty().MaximumLength(10); } } public class ServiceValidator : AbstractValidator<Service> { public ServiceValidator() { RuleFor(service => service.SeriNo) .NotEmpty().Matches("^[0-9]*$").WithMessage("Seri No must be number!") .When(service => service.ServiceType == Service.Type.OnikiBinBakım); } } } |
BikeValidator.cs:
- RuleFor(bike => bike.Model).NotEmpty() : Bike sınıfına ait Model kolonu boş geçilemez.
- RuleFor(bike => bike.Year).NotEmpty().Matches(“^[0-9]*$”).WithMessage(“Year must be number!”): Bike sınıfına ait Year kolonu boş geçilemez ve RegEx ile tanımlanan mutlaka sayısal bir alan olmalıdır.
- RuleFor(bike => bike.Serino).NotEmpty().MaximumLength(10): Yine Bike sınıfına ait Serino boş olamaz ve maximum uzunluğu 10’dur.
ServiceValidator.cs:
- RuleFor(service => service.SeriNo) .NotEmpty().Matches(“^[0-9]*$”).WithMessage(“Seri No must be number!”) .When(service => service.ServiceType == Service.Type.OnikiBinBakım) : Service sınıfına ait SeriNo kolonu, Services Tiplerinden OnikiBinBakım olduğu zaman boş geçilemez ve sayısal bir alan olması gerekmektedir.
Not : Kısaca bir sınıfa ait validation işlemi, bir koşula bağlanabilmektedir.
FluentValidation Methodları :
Pages/BikeService.razor: Sıra geldi blazor ile kayıt işleminin yapıldığı sayfaya.
- “@using bikeservice.Data” : Validation sınıfları sayfaya import edilir.
- “<EditForm Model=”customer” OnValidSubmit=”SaveCustomer”>” : Submit edilecek formun açıldığı tag’dir. Form-Submit işleminde SaveCustomer() methodu çağrılır.
- “<FluentValidator TValidator=”CustomerValidator” />” : Form üzerinde bind olan elementleri validationları,
<FluentValidator>
component’u ile yapılmaktadır. Bu Blazor template’i ile gelen bir kod parçası değildir. Aşağıda bulunan FluentValidator.cs‘in ayrıca projeye dahil edilmesi gerekmektedir. - “<InputText placeholder=”First name” @bind-Value=”customer.Name” />”: Text alan “Customer” tablosunda “Name” alanına bağlanmaktadır.
- “<ValidationMessage For=”@(() => customer.Name)” />” : Customer tablosunda Name alanında validation sırasında, bulunan hatanın mesajı bu tanımlı taglar arasında yazılır.
- “[<a href=”javascript: void(0)” @onclick=”AddService”>Add Service Record</a>]” : Burada link yerine button konulması durumunda, tüm validationlar çalışır idi. Bunun yerine “a href” koyuldu ve tıklandığında yönlenme olmaması için “javascript: void(0)” komutu yazıldı. @onclick=”AddService” tıklandığında “AddService()” methodu çağrıldı. İlgili method’da yaratılan boş service kaydı, customer.Services listesine eklendi.
- “@foreach (var service in customer.Services)” : Customer için açılmış her bir servis kaydı, ekrana basılır.
- “<InputSelect @bind-Value=”service.ServiceType”>” : Servis tipi, service sınıfının ServiceType property’sine combo araclı ile bağlanır.
- “@if (service.ServiceType == Service.Type.OnikiBinBakım)” : Servis tipi “OnikiBinBakım” olduğu zaman, “SeriNo” alanı ekrana gelmektedir. Yukarıda ServiceValidator‘da tanımladığı gibi, ancak OnikiBinBakım seçeneği seçilmesi durumunda, SeriNo zorunlu ve sayısal bir alandır.
- “<button type=”button” @onclick=”@(() => customer.Services.Remove(service))”>Remove</button>” : İlgili button tıklandığında, seçilen service customer.Services listesinden çıkarılır.
- “<ValidationMessage For=”@(() => service.SeriNo)” />” : SeriNo ile oluşabilecek, tüm hata mesajlarının yazıldığı yerdir.
- “@code {” : ServerSide şeklinde tanımlayabileceğimiz, kodun yazıldığı kısımdır.
- “SaveCustomer()” : Tüm validasyon işlemleri sağlanıp, form post olduğunda çağrılan methoddur.
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 |
@page "/bikeservice" @using bikeservice.Data @using FluentValidation <h1>Coder Bike Service</h1> <p>Customer Service Records</p> <EditForm Model="customer" OnValidSubmit="SaveCustomer"> <FluentValidator TValidator="CustomerValidator" /> <h3>Customer Name</h3> <InputText placeholder="First name" @bind-Value="customer.Name" /> <InputText placeholder="Last name" @bind-Value="customer.Surname" /> <ValidationMessage For="@(() => customer.Name)" /> <ValidationMessage For="@(() => customer.Surname)" /> <h3>Customer Bike</h3> <div> <InputText placeholder="Model" @bind-Value="customer.Bike.Model" /> <ValidationMessage For="@(() => customer.Bike.Model)" /> </div> <div> <InputText placeholder="Year" @bind-Value="customer.Bike.Year" /> <ValidationMessage For="@(() => customer.Bike.Year)" /> </div> <div> <InputText placeholder="Seri No" @bind-Value="customer.Bike.Serino" /> <ValidationMessage For="@(() => customer.Bike.Serino)" /> </div> <h3> Service List [<a href="javascript: void(0)" @onclick="AddService">Add Service Record</a>] </h3> <ValidationMessage For="@(() => customer.Services)" /> @foreach (var service in customer.Services) { <p> <InputSelect @bind-Value="service.ServiceType"> <option value="@Service.Type.BinBakım">Bin Bakım</option> <option value="@Service.Type.AltıBinBakım">AltıBin Bakım</option> <option value="@Service.Type.OnikiBinBakım">OnikiBin Bakım</option> </InputSelect> @if (service.ServiceType == Service.Type.OnikiBinBakım) { <InputText placeholder="Seri No" @bind-Value="service.SeriNo" /> } else { <span>You don't need Service Number!</span> } <button type="button" @onclick="@(() => customer.Services.Remove(service))">Remove</button> <ValidationMessage For="@(() => service.SeriNo)" /> </p> } <p><button type="submit">Submit</button></p> </EditForm> @code { private Customer customer = new Customer(); void AddService() { customer.Services.Add(new Service()); } void SaveCustomer() { Console.WriteLine("TODO: Actually do something with the valid data"); } } |
FluentValidator.cs: Bu sınıf, Blazor template’i ile gelen bir kod parçası değildir. Olduğu gibi alınıp projeye dahil edilebilir.
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 |
using FluentValidation; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using System; namespace FluentValidation { public class FluentValidator<TValidator> : ComponentBase where TValidator: IValidator, new() { private readonly static char[] separators = new[] { '.', '[' }; private TValidator validator; [CascadingParameter] private EditContext EditContext { get; set; } protected override void OnInitialized() { validator = new TValidator(); var messages = new ValidationMessageStore(EditContext); // Revalidate when any field changes, or if the entire form requests validation // (e.g., on submit) EditContext.OnFieldChanged += (sender, eventArgs) => ValidateModel((EditContext)sender, messages); EditContext.OnValidationRequested += (sender, eventArgs) => ValidateModel((EditContext)sender, messages); } private void ValidateModel(EditContext editContext, ValidationMessageStore messages) { var validationResult = validator.Validate(editContext.Model); messages.Clear(); foreach (var error in validationResult.Errors) { var fieldIdentifier = ToFieldIdentifier(editContext, error.PropertyName); messages.Add(fieldIdentifier, error.ErrorMessage); } editContext.NotifyValidationStateChanged(); } private static FieldIdentifier ToFieldIdentifier(EditContext editContext, string propertyPath) { // This method parses property paths like 'SomeProp.MyCollection[123].ChildProp' // and returns a FieldIdentifier which is an (instance, propName) pair. For example, // it would return the pair (SomeProp.MyCollection[123], "ChildProp"). It traverses // as far into the propertyPath as it can go until it finds any null instance. var obj = editContext.Model; while (true) { var nextTokenEnd = propertyPath.IndexOfAny(separators); if (nextTokenEnd < 0) { return new FieldIdentifier(obj, propertyPath); } var nextToken = propertyPath.Substring(0, nextTokenEnd); propertyPath = propertyPath.Substring(nextTokenEnd + 1); object newObj; if (nextToken.EndsWith("]")) { // It's an indexer // This code assumes C# conventions (one indexer named Item with one param) nextToken = nextToken.Substring(0, nextToken.Length - 1); var prop = obj.GetType().GetProperty("Item"); var indexerType = prop.GetIndexParameters()[0].ParameterType; var indexerValue = Convert.ChangeType(nextToken, indexerType); newObj = prop.GetValue(obj, new object[] { indexerValue }); } else { // It's a regular property var prop = obj.GetType().GetProperty(nextToken); if (prop == null) { throw new InvalidOperationException($"Could not find property named {nextToken} on object of type {obj.GetType().FullName}."); } newObj = prop.GetValue(obj); } if (newObj == null) { // This is as far as we can go return new FieldIdentifier(obj, nextToken); } obj = newObj; } } } } |
Bu makalede Blazor üzerinde, bir form kaydedilirken ilgili input alanların kontrolünü, Fluent Validation kütüphanesi kullanarak yaptık. Fluent Validation kütüphanesi, henüz Blazer proje template’i ile birlikte gelmemektedir. Bu projede custom olarak FluentValidator.cs eklenmiştir. Kod tarafında, ilgili propertylere bir veya birden fazla kontrolün, istenir ise belli koşulara bağlanarak atanabilmesi, hem kodun okunaklığını arttırır, hem de genişletilebilir, esnek olmasını sağlar. Bu da ilerde kodun kolaylıkla değiştirilebilmesine ve istenen yeni özellikler hızlıca kodlanarak eklenebilmesine yol açar. Makalenin kodlarına, aşağıdaki github adresinden erişebilirsiniz.
Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinizi hoşçakalın.
Github: https://github.com/borakasmer/BlazorFluentValidation
Source :
Son Yorumlar