March 2019

Volume 34 Number 3

[Web Development]

Full Stack C# with Blazor

By Jonathan C. Miller | March 2019 | Get the Code

Blazor, Microsoft’s experimental framework that brings C# into the browser, is the missing piece in the C# puzzle. Today, a C# programmer can write desktop, server-side Web, cloud, phone, tablet, watch, TV and IoT applications. Blazor completes the puzzle, allowing a C# developer to share code and business logic right into the user’s browser. This is a powerful ability and a gigantic productivity improvement for C# developers.

In this article, I’m going to demonstrate a common-use case for code sharing. I’ll demonstrate how to share validation logic between a Blazor client and a WebAPI server application. Today it’s expected that you validate the input not only on the server but also in the client browser. Users of modern Web applications expect near-real-time feedback. The days of filling out a long form and clicking Submit only to see a red error returned are mostly behind us.

A Blazor Web application running inside the browser can share code with a C# back-end server. You can place your logic in a shared library and utilize it on the front and back ends. This has a lot of benefits. You can put all the rules in one place and know that they only have to be updated in one location. You know that they’ll really work the same because they’re the same code. You save a bunch of time in testing and troubleshooting issues where the client and server logic aren’t always quite the same.

Perhaps most notable, you can use one library for validation on both the client and the server. Traditionally, a JavaScript front end forces developers to write two versions of validation rules—one in JavaScript for the front end and another in the language used on the back end. Attempts to solve this mismatch involve complicated rules frameworks and additional layers of abstraction. With Blazor, the same .NET Core library runs on the client and server.

Blazor is still an experimental framework, but it’s moving forward quickly. Before building this sample, make sure you have the correct version of Visual Studio, .NET Core SDK and Blazor language services installed. Please review the Getting Started steps on blazor.net.

Creating a New Blazor Application

First, let’s create a new Blazor application. From the New Project dialog box click ASP.NET Core Web Application, click OK, then select the Blazor icon in the dialog box shown in Figure 1. Click OK. This will create the default Blazor sample application. If you’ve experimented with Blazor already, this default application will be familiar to you.

Choosing a Blazor Application
Figure 1 Choosing a Blazor Application

The shared logic that validates business rules will be demonstrated on a new registration form. Figure 2 shows a simple form with fields for First Name, Last Name, Email and Phone. In this sample, it will validate that all the fields are required, that the name fields have a maximum length, and that the e-mail and phone number fields are in the correct format. It will display an error message under each field, and those messages will update as the user types. Last, the Register button will only be enabled if there are no errors.

Registration Form
Figure 2 Registration Form

Shared Library

All of the code that needs to be shared between the server and Blazor client will be placed in a separate shared library project. The shared library will contain the model class and a very simple validation engine. The model class will hold the data fields on the registration form. It looks like this:

public class RegistrationData : ModelBase
{
  [RequiredRule]
  [MaxLengthRule(50)]
  public String FirstName { get; set; }
 
  [RequiredRule]
  [MaxLengthRule(50)]
  public String LastName { get; set; }
 
  [EmailRule]
  public String Email { get; set; }
 
  [PhoneRule]
  public String Phone { get; set; }
 
}

The RegistrationData class inherits from a ModelBase class, which contains all of the logic that can be used to validate the rules and return error messages that are bound to the Blazor page. Each field is decorated with attributes that map to validation rules. I chose to create a very simple model that feels a lot like the Entity Framework (EF) Data Annotations model. All of the logic for this model is contained in the shared library.

The ModelBase class contains methods that can be used by the Blazor client application or the server application to determine if there are any validation errors. It will also fire an event when the model is changed, so the client can update the UI. Any model class can inherit from it and get all of the validation engine logic automatically.

I’ll start by first creating a new ModelBase class inside of the SharedLibrary project, like so:

public class ModelBase
{
}

Errors and Rules

Now I’ll add a private dictionary to the ModelBase class that contains a list of validation errors. The _errors dictionary is keyed by the field name and then by the rule name. The value is the actual error message to be displayed. This setup makes it easy to determine if there are validation errors for a specific field and to retrieve the error messages quickly. Here’s the code:

private Dictionary<String, Dictionary<String, String>> _errors =
  new Dictionary<string, Dictionary<string, string>>();

Now I’ll add the AddError method for entering errors into the internal errors dictionary. AddError has parameters for fieldName, ruleName and errorText. It searches the internal errors dictionary and removes entries if they already exist, then adds the new error entry, as shown in this code:

private void AddError(String fieldName, String ruleName, String errorText)
{
  if (!_errors.ContainsKey(fieldName)) { _errors.Add(fieldName,
    new Dictionary<string, string>()); }
  if (_errors[fieldName].ContainsKey(ruleName))
     { _errors[fieldName].Remove(ruleName); }
  _errors[fieldName].Add(ruleName, errorText);
  OnModelChanged();
}

Finally, I’ll add the RemoveError method, which accepts the fieldName and ruleName parameters and searches the internal errors dictionary for a matching error and removes it. Here’s the code:

private void RemoveError(String fieldName, String ruleName)
{
  if (!_errors.ContainsKey(fieldName)) { _errors.Add(fieldName,
    new Dictionary<string, string>()); }
  if (_errors[fieldName].ContainsKey(ruleName))
     { _errors[fieldName].Remove(ruleName);
    OnModelChanged();
  }
}

The next step is to add the CheckRules functions that does the work of finding the validation rules attached to the model and executing them. There are two different CheckRules functions: One that lacks a parameter and checks all rules on all fields, and a second that has a fieldName parameter and only validates a specific field. This second function is used when a field is updated, and the rules for that field are validated immediately.

The CheckRules function uses reflection to find the list of attributes attached to a field. Then, it tests each attribute to see if it’s a type of IModelRule. When an IModelRule is found, it calls the Validate method and returns the result, as shown in Figure 3.

Figure 3 The CheckRules Function

public void CheckRules(String fieldName)
{
  var propertyInfo = this.GetType().GetProperty(fieldName);
  var attrInfos = propertyInfo.GetCustomAttributes(true);
  foreach (var attrInfo in attrInfos)
  {
    if (attrInfo is IModelRule modelrule)
    {
      var value = propertyInfo.GetValue(this);
      var result = modelrule.Validate(fieldName, value);
      if (result.IsValid)
      {
        RemoveError(fieldName, attrInfo.GetType().Name);
      }
      else
      {
        AddError(fieldName, attrInfo.GetType().Name, result.Message);
      }
    }
  }
}
 
public bool CheckRules()
{
  foreach (var propInfo in this.GetType().GetProperties(
    System.Reflection.BindingFlags.Public |
      System.Reflection.BindingFlags.Instance))
    CheckRules(propInfo.Name);
 
  return HasErrors();
}

Next, I add the Errors function. This function takes a field name as a parameter and returns a string that contains the list of errors for that field. It uses the internal _errors dictionary to determine if there are any errors for that field, as shown here:

public String Errors(String fieldName)
{
  if (!_errors.ContainsKey(fieldName)) { _errors.Add(fieldName,
    new Dictionary<string, string>()); }
  System.Text.StringBuilder sb = new System.Text.StringBuilder();
  foreach (var value in _errors[fieldName].Values)
    sb.AppendLine(value);
 
  return sb.ToString();
}

Now, I need to add the HasErrors function, which returns true if there are any errors on any field of the model. This method is used by the client to determine if the Register button should be enabled. It’s also used by the WebAPI server to determine if the incoming model data has errors. Here’s the function code:

public bool HasErrors()
{
  foreach (var key in _errors.Keys)
    if (_errors[key].Keys.Count > 0) { return true; }
  return false;
}

Values and Events

It’s time to add the GetValue method, which takes a fieldname parameter and uses reflection to find the field in the model and return its value. This is used by the Blazor client to retrieve the current value and display it in the input box, as shown right here:

public String GetValue(String fieldName)
{
  var propertyInfo = this.GetType().GetProperty(fieldName);
  var value = propertyInfo.GetValue(this);
 
  if (value != null) { return value.ToString(); }
  return String.Empty;           
}

Now add the SetValue method. It uses reflection to find the field in the model and update its value. It then fires off the CheckRules method that validates all the rules on the field. It’s used in the Blazor client to update the value as the user types in the input textbox. Here’s the code:

public void SetValue(String fieldName, object value)
{
  var propertyInfo = this.GetType().GetProperty(fieldName);
  propertyInfo.SetValue(this, value);
  CheckRules(fieldName);
}

Finally, I add the event for ModelChanged, which is raised when a value on the model has been changed or a validation rule has been added or removed from the internal dictionary of errors. The Blazor client listens for this event and updates the UI when it fires. This is what causes the errors displayed to update, as shown in this code:

public event EventHandler<EventArgs> ModelChanged;
 
protected void OnModelChanged()
{
  ModelChanged?.Invoke(this, new EventArgs());
}

This validation engine is admittedly a very simple design with lots of opportunities for improvement. In a production-business application, it would be useful to have severity levels for the errors, such as Info, Warning and Error. In certain scenarios, it would be helpful if the rules could be loaded dynamically from a configuration file without the need to modify the code. I’m not advocating that you create your own validation engine; there are a lot of choices out there. This one is designed to be good enough to demo a real-world example, but simple enough to make it fit into this article and be easy to understand.

Making the Rules

At this point, there’s a RegistrationData class that contains the form fields. The fields in the class are decorated with attributes such as RequiredRule and EmailRule. The RegistrationData class inherits from a ModelBase class that contains all the logic to validate the rules and to notify the client of changes. The last piece of the validation engine is the rule logic itself. I’ll explore that next.

I start by creating a new class in the SharedLibrary called IModel­Rule. This rule consists of a single Validate method that returns a ValidationResult. Every rule must implement the IModelRule interface, as shown here:

public interface IModelRule
{
  ValidationResult Validate(String fieldName, object fieldValue);
}

Next, I create a new class in the SharedLibrary called Validation­Result, which consists of two fields. The IsValid field tells you whether the rule is valid or not, while the Message field contains the error message to be displayed when the rule is invalid. Here’s that code:

public class ValidationResult
{
  public bool IsValid { get; set; }
  public String Message { get; set; }
}

The sample application uses four different rules, all of which are public classes that inherit from the Attribute class and implement the IModelRule interface.

Now it’s time to create the rules. Keep in mind that all validation rules are simply classes that inherit from the Attribute class and implement the IModelRule interface’s Validate method. The max-length rule in Figure 4 returns an error if the text entered is longer than the specified maximum length. The other rules, for Required, Phone and Email, work similarly, but with different logic for the type of data they validate.

Figure 4 The MaxLengthRule Class

public class MaxLengthRule : Attribute, IModelRule
{
  private int _maxLength = 0;
  public MaxLengthRule(int maxLength) { _maxLength = maxLength; }
 
  public ValidationResult Validate(string fieldName, object fieldValue)
  {
    var message = $"Cannot be longer than {_maxLength} characters";
    if (fieldValue == null) { return new ValidationResult() { IsValid = true }; }
 
    var stringvalue = fieldValue.ToString();
    if (stringvalue.Length > _maxLength )
    {
      return new ValidationResult() { IsValid = false, Message = message };
    }
    else
    {
      return new ValidationResult() { IsValid = true };
    }
  }
}

Creating the Blazor Registration Form

Now that the validation engine is complete in the shared library, it can be applied to a new registration form in the Blazor application. I start by first adding a reference to the shared-library project from the Blazor application. You do this from the Solution window of the Reference Manager dialog box, as shown in Figure 5.

Adding a Reference to the Shared Library
Figure 5 Adding a Reference to the Shared Library

Next, I add a new navigation link to the application’s NavMenu. I open the Shared\NavMenu.cshtml file and add a new Registration Form link to the list, as shown in Figure 6.

Figure 6 Adding a Registration Form Link

<div class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu>
  <ul class="nav flex-column">
    <li class="nav-item px-3">
      <NavLink class="nav-link" href="" Match=NavLinkMatch.All>
        <span class="oi oi-home" aria-hidden="true"></span> Home
      </NavLink>
    </li>
    <li class="nav-item px-3">
      <NavLink class="nav-link" href="counter">
        <span class="oi oi-plus" aria-hidden="true"></span> Counter
      </NavLink>
    </li>
    <li class="nav-item px-3">
      <NavLink class="nav-link" href="fetchdata">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
      </NavLink>
    </li>
    <li class="nav-item px-3">
      <NavLink class="nav-link" href="registrationform">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Registration Form
      </NavLink>
    </li>
  </ul>
</div>

Finally, I add the new RegistrationForm.cshtml file in the Pages folder. You do this with the code shown in Figure 7.

The cshtml code in Figure 7 includes four <TextInput> fields inside the <form> tag. The <TextInput> tag is a custom Blazor component that handles the data binding and error-display logic for the field. The component only needs three parameters to work:

  • Model field: Identifies the class it’s data-bound to.
  • FieldName: Identifies the data member to data bind to.
  • DisplayName field: Enables the component to display user-friendly messages.

Figure 7 Adding the RegistrationForm.cshtml File

@page "/registrationform"
@inject HttpClient Http
@using SharedLibrary
 
<h1>Registration Form</h1>
 
@if (!registrationComplete)
{
  <form>
    <div class="form-group">
      <TextInput Model="model" FieldName="FirstName" DisplayName="First Name" />
    </div>
    <div class="form-group">
      <TextInput Model="model" FieldName="LastName" DisplayName="Last Name" />
    </div>
    <div class="form-group">
      <TextInput Model="model" FieldName="Email" DisplayName="Email" />
    </div>
    <div class="form-group">
      <TextInput Model="model" FieldName="Phone" DisplayName="Phone" />
    </div>
 
    <button type="button" class="btn btn-primary" onclick="@Register"
      disabled="@model.HasErrors()">Register</button>
  </form>
}
else
{
  <h2>Registration Complete!</h2>
}
 
@functions {
  bool registrationComplete = false;
  RegistrationData model { get; set; }
 
  protected override void OnInit()
  {
    base.OnInit();
    model = new RegistrationData() { FirstName =
      "test", LastName = "test", Email = "test@test.com", Phone = "1234567890" };
    model.ModelChanged += ModelChanged;
    model.CheckRules();
  }
 
  private void ModelChanged(object sender, EventArgs e)
  {
    base.StateHasChanged();
  }
 
  async Task Register()
  {
    await Http.PostJsonAsync<RegistrationData>(
      "https://localhost:44332/api/Registration", model);
    registrationComplete = true;
  }
}

Inside the @functions block of the page, the code is minimal. The OnInit method initializes the model class with some test data inside it. It binds to the ModelChanged event and calls the CheckRules method to validate the rules. The ModelChanged handler calls the base.StateHasChanged method to force a UI refresh. The Register method is called when the Register button is clicked, and it sends the registration data to a back-end WebAPI service.

The TextInput component contains the input label, the input textbox, the validation error message and the logic to update the model as the user types. Blazor components are very simple to write and provide a powerful way to decompose an interface into reusable parts. The parameter members are decorated with the Parameter attribute, letting Blazor know that they’re component parameters.

The input textbox’s oninput event is wired to the OnFieldChanged handler. It fires every time the input changes. The OnFieldChanged handler then calls the SetValue method, which causes the rules for that field to be executed, and the error message to be updated in real time as the user types. Figure 8 shows the code.

Figure 8 Updating the Error Message

@using SharedLibrary
 
<label>@DisplayName</label>
<input type="text" class="form-control" placeholder="@DisplayName"
  oninput="@(e => OnFieldChanged(e.Value))"
  value="@Model.GetValue(FieldName)" />
<small class="form-text" style="color:darkred;">@Model.Errors(FieldName)
  </small>
 
@functions {
 
  [Parameter]
  ModelBase Model { get; set; }
 
  [Parameter]
  String FieldName { get; set; }
 
  [Parameter]
  String DisplayName { get; set; }
 
  public void OnFieldChanged(object value)
  {
    Model.SetValue(FieldName, value);
  }
}

Validation on the Server

The validation engine is now wired up and working on the client. The next step is to use the shared library and the validation engine on the server. To do this, I start by adding another ASP.NET Core Web Application project to the solution. This time I choose API instead of Blazor in the New ASP.NET Core Web Application dialog box shown in Figure 1.

Once the new API project is created, I add a reference to the shared project, just as I did in the Blazor client application (see Figure 5). Next, I add a new controller to the API project. The new controller will accept the RegistrationData call from the Blazor client, as shown in Figure 9. The registration controller runs on the server and is typical of a back-end API server. The difference here is that it now runs the same validation rules that run on the client.

Figure 9 The Registration Controller

[Route("api/Registration")]
[ApiController]
public class RegistrationController : ControllerBase
{
  [HttpPost]
  public IActionResult Post([FromBody] RegistrationData value)
  {
    if (value.HasErrors())
    {
      return BadRequest();
    }
    // TODO: Save data to database
    return Created("api/registration", value);
  }
}

The registration controller has a single POST method that accepts the RegistrationData as its value. It calls the HasErrors method, which validates all the rules and returns a Boolean. If there are errors, the controller returns a BadRequest response; otherwise, it returns a success response. I’ve intentionally left out the code that would save the registration data to a database so I can focus on the validation scenario. The shared validation logic now runs on the client and server.

The Big Picture

This simple example of sharing validation logic in the browser and the back end barely scratches the surface of what’s possible in a full-stack C# environment. The magic of Blazor is that it allows the army of existing C# developers to build powerful, modern and responsive single-page applications with a minimal ramp-up period. It allows businesses to reuse and repackage existing code so it can run right in the browser. The ability to share C# code among browser, desktop, server, cloud and mobile platforms will greatly increase developer productivity. It will also allow developers to deliver more features and more business value to customers faster.

 


Jonathan Miller is a senior architect. He’s been developing products on the Microsoft stack for a decade and programming on .NET since its inception. Miller is a full-stack product developer with expertise in front-end technologies (Windows Forms, Windows Presentation Foundation, Silverlight, ASP.NET, Angular/Bootstrap), middleware (Windows services, Web API), and back ends (SQL server, Azure).

Thanks to the following technical expert for reviewing this article:Dino Esposito


Discuss this article in the MSDN Magazine forum