Shared Front/Back End Models and Validation with Blazor WebAssembly

Introduction:

As a Full Stack Developer (Angular 2+/C#), I can confidently say wiring up my front and back ends can be a challenge.  When doing so, I must ensure that my TypeScript models have the same properties and equivalent data types as their C# counterparts.  And while there are tools, such as Typewriter, that could handle this for me I still have to make sure that my model data is properly vetted on both the front AND back ends.  In other words, I need to write two sets of code that accomplishes the same tasks, but in different languages.  For the longest time there was no getting around this redundant busy work, but thanks to Blazor WebAssembly we can change that.

 

What is Blazor WebAssembly:

Blazor WebAssembly (Blazor WASM for short) is front end development framework that allows programmers to create UIs in C# and execute them in the browser.  This presents an advantage over traditional front end frameworks (which are written in JavaScript), as it allows developers to primarily focus on working with one programming language.  It also gives us the benefit of using a Shared project which both our front and back ends can utilize. 

 

Goal of This Tutorial:

In this blog post, I will be covering how a developer can use Blazor WASM to perform the following tasks:

  • Share models between front and back ends.
  • Write validation for each model that both front and back ends can use.

 

This helps take out much of the standard busy work, as we will only need to write our models and validation once. 

You may ask, “hold on, Whit!  You’re baking both your models and validation into a single Shared project?  Doesn’t that break with the separations of concerns?  After all, our models should have no knowledge of our business logic”.  Well, yes and no.  Microsoft already allows us to use Data Annotation decorators to do basic validation for our models with the System.ComponentModel.DataAnnotations library.  For example, RequiredAttribute allows us to specify if a property in a model is required, and PhoneAttribute allows us to check if the value provided to us is a well-formed phone number.  The work we will be doing here allows us to create our own custom decorators and call them the same way in our models.  I also feel validation and business logic are not entirely the same thing.  By giving your models the power to check themselves, you are only giving them enough power to ask “am I valid in my current state? “.  What you do with the data should still be abstracted from your model and included in a Unit of Work. 

I also want to stress that the application I am using, named QuestionEngine, is far from finished at the time of this writing.  I expect the app to evolve over time as development progresses and I become more aware of best practices for Blazor WASM.  I will be using as little code as possible to demonstrate that both front and back ends work, as my goal here is to demo that shared models and custom validation can be used. I will keep a branch open that reflects the app the state it was at for the time of this writing: 
https://github.com/whit-wu/QuestionEngine/tree/SharedModelsAndValidation

 

Note Regarding the Content:

The implementation seen in this post was learned from archived MS documentation from 2019, which can be found here:
https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/march/web-development-full-stack-csharp-with-blazor

This was written before .NET Core 3.1 was released, meaning Blazor WASM was not production ready at the time.  It has undergone significant changes since then, so I had to make a few tweaks to get the implementation working.  Besides that, much of the code is a simply copypasta from the project the docs reference to my QuestionEngine app.

 

Getting Started:

To take advantage of shared validation, you need to make sure you have a Shared project that both your front and back ends can reference.  To do this in Visual Studio, create a new Blazor WebAssembly project and make sure to have “ASP.NET Core Hosted” checked. 

This will generate two extra projects (Server and Shared) to accompany your Client project.  In my sample app, the projects QuestionEngine.Client and QuestionEngine.Server reference QuestionEngine.Shared.

 

Setting Up ModelBase and Rule Dependenices in the Shared Project

Your first order of business is setting up models and the rules (validation) you wish for them to adhere to.  Create two new directories in you Shared project, named Models and Rules.   In the Rules directory, create a new class, called ValidationResult, and insert the following code:

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

As the name implies, ValidationResult lets us know if our model is valid, which is shown with the IsValid property.  If that is false, a message will be placed in the Message property to inform us why the model is invalid.

Next, create a new interface in the Rules directory, called IModelRule, and insert the following code:

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

As you can see, we reference the ValidationResult class we just created.  All our rule classes with inherit from this interface, as each rule should return a validation result. 

Now it’s time to create the model base that all our models will inherit from.  In the Models directory, create a new class called ModelBase.cs and insert the code below:

using QuestionEngine.Shared.Rules;
using System;
using System.Collections.Generic;

namespace QuestionEngine.Shared.Models
{
    public class ModelBase
    {
        public DateTime CreatedOn { get; set; }
        public DateTime LastModifiedOn { get; set; }
        public string CreatedBy { get; set; }
        public string LastModifiedBy { get; set; }

        // the code below is a copy pasta of a project from this tutorial
        // https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/march/web-development-full-stack-csharp-with-blazor

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

        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;
        }
        public void SetValue(String fieldName, object value)
        {
            var propertyInfo = this.GetType().GetProperty(fieldName);
            propertyInfo.SetValue(this, value);
            CheckRules(fieldName);
        }

        
        // find a way to call this after error has been removed
        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();
        }

        public bool HasErrors(String fieldName)
        {
            if (!_errors.ContainsKey(fieldName)) { _errors.Add(fieldName, new Dictionary<string, string>()); }
            return (_errors[fieldName].Values.Count > 0);
        }

        public bool CheckRules()
        {
            foreach (var propInfo in this.GetType().GetProperties(System.Reflection.BindingFlags.Public |     System.Reflection.BindingFlags.Instance))
                CheckRules(propInfo.Name);

            return HasErrors();
        }

        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);
                    }
                }
            }

        }

        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();
        }

        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();
            }
        }

        public bool HasErrors()
        {
            int errorcount = 0;
            foreach (var key in _errors.Keys)
                errorcount += _errors[key].Keys.Count;
            return (errorcount != 0);
        }

        public event EventHandler<EventArgs> ModelChanged;

        protected void OnModelChanged()
        {
            ModelChanged?.Invoke(this, new EventArgs());
        }
    }
}

As the comments state, most of this is copypasta from the archived MS docs I spoke of earlier.  The methods in ModelBase will call our rules and help us understand if the given user input is valid.  If so, the IsValid property of ValidationResult will return true, otherwise it will return false with a message indicating the issue.  Now that our dependencies are setup, it’s time to make our rules!

 

Creating Rules for Each Model

As stated earlier, the System.ComponentModel.DataAnnotations library allows us to do some basic validation by using decorators (which act as rules) over the properties we wish to validate.  In this tutorial, our custom rules will be called out in the exact same fashion.  Since I want to keep things trimmed down, I am only going to show off one rule (and proof it works) used against only one of the models from my test app, the Question model. 

First, we create a new cs file to store our rules in the Rules directory.  In my case, the file will be called QuestionRules.cs.  The intention of this file is to store all rules pertaining to a single model (though it should be possible to make a file that hold rules meant to be used across all models).  Each rule in the file will be its own class, which will be set to inherit from Attribute and IModelRule (the interface that references our ValidationResult object we created earlier).

public class QuestionDescriptionRule : Attribute, IModelRule
{
    public ValidationResult Validate(string fieldName, object fieldValue)
    {
        var message = "Question must have a valid description";

        if (fieldValue != null)
        {
            var desc = (string)fieldValue;

            if (!string.IsNullOrWhiteSpace(desc))
                return new ValidationResult() { IsValid = true};
        }

        return new ValidationResult() { IsValid = false, Message = message };

    }
}

Overall, this code simply checks if the incoming object is not null or empty.  If so, a ValidationResult object, with IsValid set to true, is returned.  Otherwise a different ValidationResult is returned with IsValid set to false and a message stating “Question must have a valid description”.

Now it’s time to make our models.  When you create a model, be sure it inherits from ModelBase.cs so any custom rules logic can be kicked off.  Below, you see my Question model inherits form Modelbase.cs and calls QuestionDescriptionRule, similarly to how we would use a data annotation, over Description as a decorator.

public class Question : ModelBase
{
    [Required]
    public int Id { get; set; }
        
    [QuestionDescriptionRule]
    public string Description { get; set; }
        
    [Required]
    [AvailableAnswerRules]
    public List<Answer> AvailableAnswers { get; set; }
        

    [Required]
    public int? ChosenAnswerId { get; set; }

}

 

Testing the Rules Work Properly

To prove my rules work as expected in the back end, I have written unit tests that contain both valid and invalid descriptions for a Question.  Below is test code for a Question where all properties are valid.

[Test]
public void AddQuestion_QuestionToAddIsValid_ReturnsTrue()
{
    // Arrange
            
    var validQuestionToAdd = new Question()
    {
        Id = 1,
        Description = "Does adding a valid question work?",
        CreatedBy = userId,
        CreatedOn = DateTime.Now,
        AvailableAnswers = new List<Answer>() {
            new Answer()
            {
                Id = 1,
                Description = "Yes",
                QuestionId = 1,
                CreatedBy = userId,
                CreatedOn = DateTime.Now,
            },
            new Answer()
            {
                Id = 2,
                Description = "No",
                QuestionId = 1,
                CreatedBy = userId,
                CreatedOn = DateTime.Now,
            }
        },
        ChosenAnswerId = 1
     };

    // Act
    var questionWasAdded = uow.AddQuestion(validQuestionToAdd);

    // Assert
    Assert.IsTrue(questionWasAdded);
}

When run, the test calls the AddQuestion method from my unit of work, which uses the CheckRules method from ModelBase.cs to kick of the rule validation logic.  If CheckRules returns false, that means there are no errors present and we can save our question.  Running the unit test shows that the question can be added successfully. 

To verify an empty description will flag an error, I have written the following unit test:

[Test]
public void AddQuestion_QuestionDescIsEmptyString_ReturnsFalse()
{
    // Arrange
    var validQuestionToAdd = new Question()
    {
        Id = 1,
        Description = string.Empty,
        CreatedBy = userId,
        CreatedOn = DateTime.Now,
        AvailableAnswers = new List<Answer>() {
            new Answer()
            {
                Id = 1,
                Description = "Yes",
                QuestionId = 1,
                CreatedBy = userId,
                CreatedOn = DateTime.Now,
            },
            new Answer()
            {
                Id = 2,
                Description = "No",
                QuestionId = 1,
                CreatedBy = userId,
                CreatedOn = DateTime.Now,
            }
        },
        ChosenAnswerId = 1
    };
    
    // Act
    var questionWasAdded = uow.AddQuestion(validQuestionToAdd);

    // Assert
    Assert.IsFalse(questionWasAdded);
}

In this instance, we would expect CheckRules to return true, which indicates we have a ValidationResult with IsValid set to false and a message telling us our description is not valid.  Running the test shows it passes with flying colors:

Even with our tests working as expected, they are only checking if the app is preventing invalid data from being saved.  Let’s switch over to the front end to prove that validation is working AND we are getting our message sent back to inform the user what went wrong.

The code below is for a Razor page (AddSurvey.razor) I created that allows users to create a survey in my QuestionEngine app (which has yet to be completed).  All that’s important to understand for now is that a Survey can have one-to-many Questions, so we would expect our validation to kick in when a Question is added.  Questions are bound to an InputText box, and in the small tag will show the error message for the Description if CheckErrors returns true.

@page "/addSurvey"
@using QuestionEngine.Shared.Models
@inject HttpClient Http
<h3>Add Survey</h3>

<EditForm Model="@surveyToAdd">
    <div>
        <label>Input Survey Name:</label>
        <InputText id="surveyName" @bind-Value="surveyToAdd.Name" />
    </div>

</EditForm>
<br />

@if (surveyToAdd != null && !string.IsNullOrWhiteSpace(surveyToAdd.Name))
{


    @if (surveyToAdd.Questions != null && surveyToAdd.Questions.Count > 0)
    {

        try
        {

            @foreach (var q in surveyToAdd.Questions)
            {

                <EditForm Model="q">
                    <div>
                        <label>Qusetion Desc:</label>
                        <InputText id="questionDesc" @bind-Value="q.Description"></InputText>
                        <small hidden="@(!q.CheckRules())" class="form-text" style="color:darkred;">@q.Errors("Description")</small>
                    </div>
                    
                    
                </EditForm>
                <br />
            }
        }
        catch (Exception ex)
        {
            throw;
        }
    }
    <button @onclick="AddQuestionToList">Add A Question</button>

}
@code {
    private Survey surveyToAdd;

    protected override void OnInitialized()
    {
        surveyToAdd = new Survey();
        surveyToAdd.Questions = new List<Question>();

    }

    private void AddQuestionToList()
    {
        try
        {
            Question question = new Question();
            surveyToAdd.Questions.Add(question);
        }
        catch (Exception ex)
        {
            throw;
        }
    }
}

Right off the bat, any Question I attempt to add returns our error message, indicating or Description is invalid. This makes sense because we have null data there at this point.

However, adding data to the fields makes the errors go away.

 

Conclusion

Blazor WASM has potential to give .NET developers the ability to perform full stack development without needing a JavaScript framework.  One of the major advantages it has to using a JS framework is that it gives developers the ability to write their models and validation logic once, cutting out the busy work of needing to do it for the front and back ends separately.   

Leave a Reply

Your email address will not be published. Required fields are marked *

I accept that my given data and my IP address is sent to a server in the USA only for the purpose of spam prevention through the Akismet program.More information on Akismet and GDPR.