Search code examples
asp.net-coreblazorbittinyintef-core-8.0

Problem with EF Core 8 handling MSSQL tinyint & nullable bit in Blazor


Following are excerpts from a Visual Studio 2022 Blazor sample project, purpose built to show the problem.

Appreciate any advice on rectifying the errors listed at the bottom. However, changing MSSQL data types is unfortunately not optable. Thanks in advance!

document: BlazorApp1.proj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter" Version="8.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.2" />
  </ItemGroup>
</Project>

document: appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "connectionstring": "Server=(localdb)\\MSSQLLocalDB;Database=Test;Trusted_Connection=True"
  }
}

document: Program.cs

using BlazorApp1.Components;

var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();
app.Run();

database:

CREATE DATABASE Test
CREATE TABLE [dbo].[Sample] (
    [Id]              INT        NOT NULL,
    [NCharRequired]   NCHAR (10) NOT NULL,
    [NCharNullable]   NCHAR (10) NULL,
    [IntRequired]     INT        NOT NULL,
    [IntNullable]     INT        NULL,
    [TinyIntRequired] TINYINT    NOT NULL,
    [TinyIntNullable] TINYINT    NULL,
    [BitRequired]     BIT        NOT NULL,
    [BitNullable]     BIT        NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC)
);

window: Package Management Console

Scaffold-DbContext "Server=(localdb)\MSSQLLocalDB;Database=Test;Trusted_Connection=True" Microsoft.EntityFrameworkCore.SqlServer

document: Sample.cs

namespace BlazorApp1.Models;
public partial class Sample
{
    public int Id { get; set; }
    public string NcharRequired { get; set; } = null!;
    public string? NcharNullable { get; set; }
    public int IntRequired { get; set; }
    public int? IntNullable { get; set; }
    public byte TinyIntRequired { get; set; }
    public byte? TinyIntNullable { get; set; }
    public bool BitRequired { get; set; }
    public bool? BitNullable { get; set; }
}

document: TestContext.cs

using Microsoft.EntityFrameworkCore;
namespace BlazorApp1.Models;

public partial class TestContext : DbContext
{
    public TestContext()
    {
    }
    public TestContext(DbContextOptions<TestContext> options)
        : base(options)
    {
    }
    public virtual DbSet<Sample> Samples { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
        => optionsBuilder.UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=Test;Trusted_Connection=True");
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Sample>(entity =>
        {
            entity.HasKey(e => e.Id).HasName("PK__Table__3214EC07F913B460");
            entity.ToTable("Sample");
            entity.Property(e => e.Id).ValueGeneratedNever();
            entity.Property(e => e.NcharNullable)
                .HasMaxLength(10)
                .IsFixedLength()
                .HasColumnName("NCharNullable");
            entity.Property(e => e.NcharRequired)
                .HasMaxLength(10)
                .IsFixedLength()
                .HasColumnName("NCharRequired");
        });
        OnModelCreatingPartial(modelBuilder);
    }
    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

context menu: BlazorApp1

Add->
New Scaffolded Item->
Razor Components using Entity Framework (CRUD)->
Template:CRUD
Model class:Sample (BlazorApp1.Models)
DbContext class:TestContext (BlazorApp1.Models)

document: Create.razor

@page "/samples/create"
@inject BlazorApp1.Models.TestContext DB
@using BlazorApp1.Models
@inject NavigationManager NavigationManager
<PageTitle>Create</PageTitle>
<h1>Create</h1>
<h4>Sample</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <EditForm method="post" Model="Sample" OnValidSubmit="AddSample" FormName="create" Enhance>
            <DataAnnotationsValidator />
            <ValidationSummary class="text-danger" />
            <div class="mb-3">
                <label for="ncharrequired" class="form-label">NcharRequired:</label>
                <InputText id="ncharrequired" @bind-Value="Sample.NcharRequired" class="form-control" />
                <ValidationMessage For="() => Sample.NcharRequired" class="text-danger" />
            </div>
            <div class="mb-3">
                <label for="ncharnullable" class="form-label">NcharNullable:</label>
                <InputText id="ncharnullable" @bind-Value="Sample.NcharNullable" class="form-control" />
                <ValidationMessage For="() => Sample.NcharNullable" class="text-danger" />
            </div>
            <div class="mb-3">
                <label for="intrequired" class="form-label">IntRequired:</label>
                <InputNumber id="intrequired" @bind-Value="Sample.IntRequired" class="form-control" />
                <ValidationMessage For="() => Sample.IntRequired" class="text-danger" />
            </div>
            <div class="mb-3">
                <label for="intnullable" class="form-label">IntNullable:</label>
                <InputNumber id="intnullable" @bind-Value="Sample.IntNullable" class="form-control" />
                <ValidationMessage For="() => Sample.IntNullable" class="text-danger" />
            </div>
            <div class="mb-3">
                <label for="tinyintrequired" class="form-label">TinyIntRequired:</label>
                <InputText id="tinyintrequired" @bind-Value="Sample.TinyIntRequired" class="form-control" />
                <ValidationMessage For="() => Sample.TinyIntRequired" class="text-danger" />
            </div>
            <div class="mb-3">
                <label for="tinyintnullable" class="form-label">TinyIntNullable:</label>
                <InputText id="tinyintnullable" @bind-Value="Sample.TinyIntNullable" class="form-control" />
                <ValidationMessage For="() => Sample.TinyIntNullable" class="text-danger" />
            </div>
            <div class="mb-3">
                <label for="bitrequired" class="form-label">BitRequired:</label>
                <InputCheckbox id="bitrequired" @bind-Value="Sample.BitRequired" class="form-check-input" />
                <ValidationMessage For="() => Sample.BitRequired" class="text-danger" />
            </div>
            <div class="mb-3">
                <label for="bitnullable" class="form-label">BitNullable:</label>
                <InputCheckbox id="bitnullable" @bind-Value="Sample.BitNullable" class="form-check-input" />
                <ValidationMessage For="() => Sample.BitNullable" class="text-danger" />
            </div>
            <button type="submit" class="btn btn-primary">Create</button>
        </EditForm>
    </div>
</div>
<div>
    <a href="/samples">Back to List</a>
</div>
@code {
    [SupplyParameterFromForm]
    public Sample Sample { get; set; } = new();
    // To protect from overposting attacks, see https://aka.ms/RazorPagesCRUD
    public async Task AddSample()
    {
        DB.Samples.Add(Sample);
        await DB.SaveChangesAsync();
        NavigationManager.NavigateTo("/samples");
    }
}

document: Edit.razor

@page "/Samples/edit"
@inject BlazorApp1.Models.TestContext DB
@using BlazorApp1.Models
@inject NavigationManager NavigationManager
@using Microsoft.EntityFrameworkCore
<PageTitle>Edit</PageTitle>
<h1>Edit</h1>
<h4>Sample</h4>
<hr />
@if (Sample is null)
{
    <p><em>Loading...</em></p>
}
else
{
    <div class="row">
        <div class="col-md-4">
            <EditForm method="post" Model="Sample" OnValidSubmit="UpdateSample" FormName="edit" Enhance>
                <DataAnnotationsValidator />
                <ValidationSummary />
                <input type="hidden" name="Sample.Id" value="@Sample.Id" />
                <div class="mb-3">
                    <label for="ncharrequired" class="form-label">NcharRequired:</label>
                    <InputText id="ncharrequired" @bind-Value="Sample.NcharRequired" class="form-control" />
                    <ValidationMessage For="() => Sample.NcharRequired" class="text-danger" />
                </div>
                <div class="mb-3">
                    <label for="ncharnullable" class="form-label">NcharNullable:</label>
                    <InputText id="ncharnullable" @bind-Value="Sample.NcharNullable" class="form-control" />
                    <ValidationMessage For="() => Sample.NcharNullable" class="text-danger" />
                </div>
                <div class="mb-3">
                    <label for="intrequired" class="form-label">IntRequired:</label>
                    <InputNumber id="intrequired" @bind-Value="Sample.IntRequired" class="form-control" />
                    <ValidationMessage For="() => Sample.IntRequired" class="text-danger" />
                </div>
                <div class="mb-3">
                    <label for="intnullable" class="form-label">IntNullable:</label>
                    <InputNumber id="intnullable" @bind-Value="Sample.IntNullable" class="form-control" />
                    <ValidationMessage For="() => Sample.IntNullable" class="text-danger" />
                </div>
                <div class="mb-3">
                    <label for="tinyintrequired" class="form-label">TinyIntRequired:</label>
                    <InputText id="tinyintrequired" @bind-Value="Sample.TinyIntRequired" class="form-control" />
                    <ValidationMessage For="() => Sample.TinyIntRequired" class="text-danger" />
                </div>
                <div class="mb-3">
                    <label for="tinyintnullable" class="form-label">TinyIntNullable:</label>
                    <InputText id="tinyintnullable" @bind-Value="Sample.TinyIntNullable" class="form-control" />
                    <ValidationMessage For="() => Sample.TinyIntNullable" class="text-danger" />
                </div>
                <div class="mb-3">
                    <label for="bitrequired" class="form-label">BitRequired:</label>
                    <InputCheckbox id="bitrequired" @bind-Value="Sample.BitRequired" class="form-check-input" />
                    <ValidationMessage For="() => Sample.BitRequired" class="text-danger" />
                </div>
                <div class="mb-3">
                    <label for="bitnullable" class="form-label">BitNullable:</label>
                    <InputCheckbox id="bitnullable" @bind-Value="Sample.BitNullable" class="form-check-input" />
                    <ValidationMessage For="() => Sample.BitNullable" class="text-danger" />
                </div>
                <button type="submit" class="btn btn-primary">Save</button>
            </EditForm>
        </div>
    </div>
}
<div>
    <a href="/samples">Back to List</a>
</div>
@code {
    [SupplyParameterFromQuery]
    public int Id { get; set; }
    [SupplyParameterFromForm]
    public Sample? Sample { get; set; }
    protected override async Task OnInitializedAsync()
    {
        Sample ??= await DB.Samples.FirstOrDefaultAsync(m => m.Id == Id);
        if (Sample is null)
        {
            NavigationManager.NavigateTo("notfound");
        }
    }
    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task UpdateSample()
    {
        DB.Attach(Sample!).State = EntityState.Modified;
        try
        {
            await DB.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!SampleExists(Sample!.Id))
            {
                NavigationManager.NavigateTo("notfound");
            }
            else
            {
                throw;
            }
        }
        NavigationManager.NavigateTo("/samples");
    }
    bool SampleExists(int id)
    {
        return DB.Samples.Any(e => e.Id == id);
    }
}

window: Error List

Error (active)  CS1662  Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type BlazorApp1  .\Create.razor  14
Error (active)  CS1503  Argument 1: cannot convert from 'byte' to 'string'  BlazorApp1  .\Create.razor  39
Error (active)  CS1503  Argument 2: cannot convert from 'Microsoft.AspNetCore.Components.EventCallback<byte>' to 'Microsoft.AspNetCore.Components.EventCallback'    BlazorApp1  .\Create.razor  39
Error (active)  CS0029  Cannot implicitly convert type 'byte' to 'string'   BlazorApp1  .\Create.razor  39
Error (active)  CS1662  Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type BlazorApp1  .\Create.razor  39
Error (active)  CS1503  Argument 1: cannot convert from 'byte?' to 'string' BlazorApp1  .\Create.razor  44
Error (active)  CS1503  Argument 2: cannot convert from 'Microsoft.AspNetCore.Components.EventCallback<byte?>' to 'Microsoft.AspNetCore.Components.EventCallback'   BlazorApp1  .\Create.razor  44
Error (active)  CS0029  Cannot implicitly convert type 'byte?' to 'string'  BlazorApp1  .\Create.razor  44
Error (active)  CS1662  Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type BlazorApp1  .\Create.razor  44
Error (active)  CS1503  Argument 1: cannot convert from 'bool?' to 'bool'   BlazorApp1  .\Create.razor  54
Error (active)  CS1503  Argument 2: cannot convert from 'Microsoft.AspNetCore.Components.EventCallback<bool?>' to 'Microsoft.AspNetCore.Components.EventCallback'   BlazorApp1  .\Create.razor  54
Error (active)  CS0266  Cannot implicitly convert type 'bool?' to 'bool'. An explicit conversion exists (are you missing a cast?)   BlazorApp1  .\Create.razor  54
Error (active)  CS1662  Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type BlazorApp1  .\Create.razor  54
Error (active)  CS1662  Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type BlazorApp1  .\Edit.razor    21
Error (active)  CS1503  Argument 1: cannot convert from 'byte' to 'string'  BlazorApp1  .\Edit.razor    47
Error (active)  CS1503  Argument 2: cannot convert from 'Microsoft.AspNetCore.Components.EventCallback<byte>' to 'Microsoft.AspNetCore.Components.EventCallback'    BlazorApp1  .\Edit.razor    47
Error (active)  CS0029  Cannot implicitly convert type 'byte' to 'string'   BlazorApp1  .\Edit.razor    47
Error (active)  CS1662  Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type BlazorApp1  .\Edit.razor    47
Error (active)  CS1503  Argument 1: cannot convert from 'byte?' to 'string' BlazorApp1  .\Edit.razor    52
Error (active)  CS1503  Argument 2: cannot convert from 'Microsoft.AspNetCore.Components.EventCallback<byte?>' to 'Microsoft.AspNetCore.Components.EventCallback'   BlazorApp1  .\Edit.razor    52
Error (active)  CS0029  Cannot implicitly convert type 'byte?' to 'string'  BlazorApp1  .\Edit.razor    52
Error (active)  CS1662  Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type BlazorApp1  .\Edit.razor    52
Error (active)  CS1503  Argument 1: cannot convert from 'bool?' to 'bool'   BlazorApp1  .\Edit.razor    62
Error (active)  CS1503  Argument 2: cannot convert from 'Microsoft.AspNetCore.Components.EventCallback<bool?>' to 'Microsoft.AspNetCore.Components.EventCallback'   BlazorApp1  .\Edit.razor    62
Error (active)  CS0266  Cannot implicitly convert type 'bool?' to 'bool'. An explicit conversion exists (are you missing a cast?)   BlazorApp1  .\Edit.razor    62
Error (active)  CS1662  Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type BlazorApp1  .\Edit.razor    62

Have tried:

  1. Searching the web extensively and browsing many related articles, but failed to find a solution.

  2. Adding these to OnModelCreating, but errors persisted.

entity.Property(e => e.TinyIntRequired).HasColumnType("tinyint").IsRequired(true);
entity.Property(e => e.TinyIntNullable).HasColumnType("tinyint").IsRequired(false);
entity.Property(e => e.BitNullable).HasColumnType("bit").IsRequired(false);
  1. Investigatively changing MSSQL data types from tinyint to int and bit null allowed to not allowed. Errors eliminated. However, changing data types is unfortunately not optable.

Solution

  • Your problem appears to be that you are trying to use Input controls that don't support the data types you are using.

    InputNumber doesn't support byte, so you need to create a version that does.

    using Microsoft.AspNetCore.Components;
    using Microsoft.AspNetCore.Components.Forms;
    using Microsoft.AspNetCore.Components.Rendering;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.Text.RegularExpressions;
    
    namespace SO78206175.Components;
    
    public class BlazrInputNumber<TValue> : InputBase<TValue>
    {
        private static readonly string _stepAttributeValue = GetStepAttributeValue();
    
        private static string GetStepAttributeValue()
        {
            // Unwrap Nullable<T>, because InputBase already deals with the Nullable aspect
            // of it for us. We will only get asked to parse the T for nonempty inputs.
            var targetType = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue);
            if (targetType == typeof(int) ||
                targetType == typeof(long) ||
                targetType == typeof(short) ||
                targetType == typeof(float) ||
                targetType == typeof(double) ||
                targetType == typeof(byte) ||
                targetType == typeof(decimal))
            {
                return "any";
            }
            else
            {
                throw new InvalidOperationException($"The type '{targetType}' is not a supported numeric type.");
            }
        }
    
        [Parameter] public string ParsingErrorMessage { get; set; } = "The {0} field must be a number.";
    
        [DisallowNull] public ElementReference? Element { get; protected set; }
    
        /// <inheritdoc />
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, "input");
            builder.AddAttribute(1, "step", _stepAttributeValue);
            builder.AddMultipleAttributes(2, AdditionalAttributes);
            builder.AddAttribute(3, "type", "number");
            if (NameAttributeValue is not null)
                builder.AddAttribute(4, "name", NameAttributeValue);
    
            if (CssClass is not null)
                builder.AddAttribute(5, "class", CssClass);
    
            builder.AddAttribute(6, "value", CurrentValueAsString);
            builder.AddAttribute(7, "onchange", EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString));
            builder.SetUpdatesAttributeName("value");
            builder.AddElementReferenceCapture(8, __inputReference => Element = __inputReference);
            builder.CloseElement();
        }
    
        protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
        {
            if (typeof(TValue) == typeof(byte))
            {
                if (!byte.TryParse(value, out byte outcome))
                {
                    result = default;
                    validationErrorMessage = "The entry exeeds the maximum or minimum value for a byte.";
                    return false;
    
                }
            }
    
            if (typeof(TValue) == typeof(byte?))
            {
                if (value is not null && !byte.TryParse(value, out byte outcome))
                {
                    result = default;
                    validationErrorMessage = "The entry exeeds the maximum or minimum value for a byte.";
                    return false;
                }
            }
    
            if (BindConverter.TryConvertTo<TValue>(value, CultureInfo.InvariantCulture, out result))
            {
                validationErrorMessage = null;
                return true;
            }
            else
            {
                validationErrorMessage = string.Format(CultureInfo.InvariantCulture, ParsingErrorMessage, DisplayName ?? FieldIdentifier.FieldName);
                return false;
            }
        }
    
        protected override string? FormatValueAsString(TValue? value)
        {
            // Avoiding a cast to IFormattable to avoid boxing.
            switch (value)
            {
                case null:
                    return null;
    
                case byte @byte:
                    return @byte.ToString(CultureInfo.CurrentCulture);
    
                case int @int:
                    return BindConverter.FormatValue(@int, CultureInfo.InvariantCulture);
    
                case long @long:
                    return BindConverter.FormatValue(@long, CultureInfo.InvariantCulture);
    
                case short @short:
                    return BindConverter.FormatValue(@short, CultureInfo.InvariantCulture);
    
                case float @float:
                    return BindConverter.FormatValue(@float, CultureInfo.InvariantCulture);
    
                case double @double:
                    return BindConverter.FormatValue(@double, CultureInfo.InvariantCulture);
    
                case decimal @decimal:
                    return BindConverter.FormatValue(@decimal, CultureInfo.InvariantCulture);
    
                default:
                    throw new InvalidOperationException($"Unsupported type {value.GetType()}");
            }
        }
    }
    

    Checkboxes don't support bool? because the're binary: a bool? has three states! You need to create a specific control. Here's an example button group control:

    <div class="form-control">
    
    <div class="btn-group" role="group" aria-label="Basic example">
        <button type="button" class="@_notSetColour" @onclick="() => OnButtonChanged(-1)"> Not Set</button>
        <button type="button" class="@_onColour" @onclick="() => OnButtonChanged(1)">On</button>
        <button type="button" class="@_offColour" @onclick="() => OnButtonChanged(0)">Off</button>
    </div>
    
    </div>
    
    @code {
        [Parameter] public bool? Value { get; set; }
        [Parameter] public EventCallback<bool?> ValueChanged { get; set; }
    
        private int _value => this.Value switch
        {
            true => 1,
            false => 0,
            _ => -1
        };
    
        private bool? GetBoolValue(int value) => value switch
        {
            1 => true,
            0 => false,
            _ => null
        };
    
        private string _notSetColour => _value == -1 ? "btn btn-secondary" : "btn btn-outline-secondary";
        private string _onColour => _value == 1 ? "btn btn-success" : "btn btn-outline-success";
        private string _offColour => _value == 0 ? "btn btn-danger" : "btn btn-outline-danger";
    
        private Task OnButtonChanged(int value)
        {
            this.ValueChanged.InvokeAsync(GetBoolValue(value));
            return Task.CompletedTask;
        }
    }
    

    My Demo page:

    @page "/"
    
    <PageTitle>Home</PageTitle>
    
    <h1>Example</h1>
    
    <EditForm EditContext="_editContext">
        <label>Byte Required</label>
        <BlazrInputNumber class="form-control mb-2" max="@byte.MaxValue" min="@byte.MinValue" @bind-Value="_model.TinyIntRequired" />
        <ValidationMessage For="() => _model.TinyIntRequired" />
        <label>Byte Nullable</label>
        <BlazrInputNumber class="form-control mb-2" max="@byte.MaxValue" min="@byte.MinValue" @bind-Value="_model.TinyIntNullable" />
        <ValidationMessage For="() => _model.TinyIntNullable" />
    
        <label>Byte Nullable</label>
        <InputBoolButtons @bind-Value="_model.BitNullable" />
    </EditForm>
    
    <div class="bg-dark text-white m-2 p-2">
        <pre>Byte Required: @_model.TinyIntRequired</pre>
        <pre>Byte Nullable: @(_model.TinyIntNullable?.ToString() ?? "Null")</pre>
        <pre>Bit Nullable: @(_model.BitNullable?.ToString() ?? "Null")</pre>
    </div>
    @code {
        private Sample _model = new();
        private EditContext? _editContext;
    
        protected override Task OnInitializedAsync()
        {
            _editContext = new(_model);
            return Task.CompletedTask;
        }
    }
    

    enter image description here

    Additional Information

    If you want to use standard edit controls you can create a edit object that you map your data object into and then out of to save.

    Here's some code to demonstrate how:

    
    public partial class Sample
    {
        public int Id { get; set; }
        public byte? TinyIntNullable { get; set; }
    }
    
    // This is the object you write the validator for
    // and you point the editor controls to
    public class SampleEditMutator
    {
        public int Id { get; private set; }
        public short? TinyIntNullable { get; set; }
    
        public SampleEditMutator(Sample record)
        {
            this.Id = record.Id;
            this.TinyIntNullable = Convert.ToInt16(record.TinyIntNullable);
        }
    
        // Call this to apply the edit changes to a Sample object
        public Sample Mutate(Sample record)
        {
            record.TinyIntNullable =  ConvertToByte(this.TinyIntNullable);
            return record;
        }
    
        private byte? ConvertToByte(short? value)
        {
            if (value is null)
                return null;
    
            if (byte.TryParse(this.TinyIntNullable.ToString(), out byte outvalue))
                throw new Exception("Not Allowed");
    
            return outvalue;
        }
    }