Skip to main content

Create a CRUD App using Blazor and ASP.NET Core

Blazor is an experimental .NET web framework using C#/Razor and HTML that runs in the browser via WebAssembly. It simplifies the process of creating single page application (SPA) and at the same time also brings the power of .NET on the table. It runs in the browser on a real .NET runtime (Mono) implemented in WebAssembly that executes normal .NET assemblies. In this post, we’ll see how to set up blazor and create a CRUD app using blazor and ASP.NET Core.

Create a CRUD App using Blazor and ASP.NET Core

This post doesn’t cover the concept of Blazor in detail and if you are unfamiliar with it, I recommend you to visit these awesome posts to make yourself comfortable with blazor.

Setting up Blazor

Setting up blazor is super easy. You are required to do the following installations.

That’s it. You’re now ready to start building web apps with blazor!

Creating a Blazor App

To create a blazor app, open Visual Studio 2017 “Preview” version, hit Ctrl+Shift+N and select the ASP.NET Core Web Application (.NET Core) project type from the templates. When you click Ok, you will get the following prompt,

Create a CRUD App using Blazor and ASP.NET Core

If you don’t see these blazor templates, make sure .NET Core and ASP.NET Core 2.0 are selected at the top. As you can see, there are 2 blazor templates. The standalone “Blazor” template will create a static blazor app and “Blazor (ASP.NET Core Hosted)” will create an ASP.NET Core hosted blazor app. Choose “Blazor (ASP.NET Core Hosted)” to create the blazor app. Now, our first Blazor app will be created. In the solution explorer, you will find 3 projects created.

  • <Project Name>.Client: Standard blazor application.
  • <Project Name>.Server: It’s an ASP.NET Core Web API project.
  • <Project Name>.Shared: It’s a .NET standard class library project used for holding the shared resources.

You should run the app to make sure that there are no errors. Press Ctrl-F5 to run the app without the debugger. Running with the debugger (F5) is not supported at this time. Next thing is to extend this app to create a CRUD app.

Create the CRUD App

Before we move on, take a look at the following image to get an idea of what we are building.

Create a CRUD App using Blazor and ASP.NET Core

As you can see, we are building a ToDo List app supporting all the CRUD operations and a listing of the ToDo items. We’ll use Entity Framework Core in-memory provider to store the ToDo list. So let’s begin.

  • First, create a ToDoList class in the Shared project with the following code. It’s a simple class with only two members.
    public class ToDoList
    {
    public int ID { get; set; }
    public string Item { get; set; }
    }
  • Next, create EF DBContext to interact with the database. To do that, create a new folder named “Data” in the Server project. Add a new class file named ToDoListContext with the following code.
    public class ToDoListContext : DbContext
    {
    public ToDoListContext(DbContextOptions<ToDoListContext> options) : base(options) { }
    public DbSet<ToDoList> ToDoLists { get; set; }
    }
    We’ll need to configure InMemory db provider. To do that, Open Startup.cs class and navigate to the ConfigureServices method to configure it (Line no. 4).
    public void ConfigureServices(IServiceCollection services)
    {
    // Use Entity Framework in-memory provider for this sample
    services.AddDbContext<ToDoListContext>(options => options.UseInMemoryDatabase("ToDoList"));
    services.AddMvc().AddJsonOptions(options =>
    {
    options.SerializerSettings.ContractResolver = new DefaultContractResolver();
    });
    services.AddResponseCompression(options =>
    {
    options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
    {
    MediaTypeNames.Application.Octet,
    WasmMediaTypeNames.Application.Wasm,
    });
    });
    }
  • Next, we’ll need an API controller for all CRUD operations. To do that, create a new controller named ToDoListController inside the Controller folder in the Server project with the following code.
    [Produces("application/json")]
    [Route("api/ToDo")]
    public class ToDoListController : Controller
    {
    private readonly ToDoListContext _context;
    public ToDoListController(ToDoListContext context)
    {
    _context = context;
    }
    // GET: api/ToDo
    [HttpGet]
    public IEnumerable<ToDoList> GetToDo()
    {
    return _context.ToDoLists;
    }
    // POST: api/ToDo
    [HttpPost]
    public async Task<IActionResult> PostToDo([FromBody] ToDoList todo)
    {
    if (!ModelState.IsValid)
    return BadRequest(ModelState);
    _context.ToDoLists.Add(todo);
    await _context.SaveChangesAsync();
    return CreatedAtAction("GetToDo", new { id = todo.ID }, todo);
    }
    // PUT: api/ToDo/5
    [HttpPut("{id}")]
    public async Task<IActionResult> PutToDo([FromRoute] int id, [FromBody] ToDoList todo)
    {
    if (!ModelState.IsValid)
    return BadRequest(ModelState);
    if (id != todo.ID)
    return BadRequest();
    _context.Entry(todo).State = EntityState.Modified;
    try
    {
    await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
    if (!ToDoExists(id))
    return NotFound();
    else
    throw;
    }
    return NoContent();
    }
    // DELETE: api/ToDo/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteToDo([FromRoute] int id)
    {
    var ToDo = await _context.ToDoLists.SingleOrDefaultAsync(m => m.ID == id);
    if (ToDo == null)
    return NotFound();
    _context.ToDoLists.Remove(ToDo);
    await _context.SaveChangesAsync();
    return Ok(ToDo);
    }
    private bool ToDoExists(int id)
    {
    return _context.ToDoLists.Any(e => e.ID == id);
    }
    }
    Here, the controller has API for all CRUD operations with proper validations. Quite familiar piece of code, isn’t it?
  • Blazor App Code

  • Next, we need to add the ToDo component to the Client App. The default app is already having Home, Counter and Fetch Data component. To add ToDo component, right-click on Pages folder and select Add > New Item. Select Web from the left panel, then select “Razor View” from templates panel and name it ToDo.cshtml. Put the following code in the file.
    @page "/todo"
    @using ASPNETBlazorCRUDApp.Shared
    @using Microsoft.AspNetCore.Blazor.Browser.Interop
    @inject HttpClient Http
    <h1>ToDo List</h1>
    <div>
    <div class="row">
    <div class="col-sm-1">
    <p>Item:</p>
    </div>
    <div class="col-sm-4">
    <input id="todoName" placeholder="Item Name" @bind(itemName)>
    </div>
    </div>
    <br />
    <div class="row">
    <div class="col-sm-1">
    <button class="btn btn-info" id="btnAdd" @onclick(async () => await AddToDo())>Add</button>
    </div>
    <div class="col-sm-2">
    @if (todos != null && todos.Count > 0)
    {
    <button class="btn btn-danger" @onclick(async () => await DeleteAllToDos())>Delete All</button>
    }
    </div>
    </div>
    </div>
    <br /><br />
    @if (todos == null)
    {
    <p><em>Loading...</em></p>
    }
    else
    {
    @if (todos.Count > 0)
    {
    <table class='table table-striped table-bordered table-hover table-condensed' style="width:80%;">
    <thead>
    <tr>
    <th style="width: 40%">Name</th>
    <th style="width: 40%">Edit</th>
    <th style="width: 20%">Delete</th>
    </tr>
    </thead>
    <tbody>
    @foreach (var todo in todos)
    {
    <tr>
    <td>
    <span id="spn_@todo.ID">@todo.Item</span>
    <input id="txt_@todo.ID" @bind(UpdateItemName) style="display:none;">
    </td>
    <td>
    <button id="btnEdit_@todo.ID" class="btn btn-primary" @onclick(() => EditToDo(todo.ID, todo.Item))>Edit</button>
    <button id="btnUpdate_@todo.ID" style="display:none;" class="btn btn-success" @onclick(async () => await UpdateToDo(todo.ID))>Update</button>
    <button id="btnCancel_@todo.ID" style="display:none;" class="btn btn-primary" @onclick(() => CancelToDo(todo.ID))>Cancel</button>
    </td>
    <td><button class="btn btn-danger" @onclick(async () => await DeleteToDo(todo.ID))>Delete</button></td>
    </tr>
    }
    </tbody>
    </table>
    }
    }
    <script>
    Blazor.registerFunction('ShowControl', (id, item, bShow) => {
    if (bShow) {
    var txtInput = document.getElementById("txt_" + id);
    document.getElementById("spn_" + id).style.display = "none";
    txtInput.style.display = "";
    txtInput.value = item;
    txtInput.focus();
    document.getElementById("btnEdit_" + id).style.display = "none";
    document.getElementById("btnUpdate_" + id).style.display = "";
    document.getElementById("btnCancel_" + id).style.display = "";
    }
    else {
    document.getElementById("spn_" + id).style.display = "";
    document.getElementById("txt_" + id).style.display = "none";
    document.getElementById("btnEdit_" + id).style.display = "";
    document.getElementById("btnUpdate_" + id).style.display = "none";
    document.getElementById("btnCancel_" + id).style.display = "none";
    }
    return true;
    });
    </script>
    @functions {
    string itemName;
    string UpdateItemName;
    IList<ToDoList> todos = new List<ToDoList>();
    protected override async Task OnInitAsync()
    {
    await Refresh();
    }
    private async Task Refresh()
    {
    todos = await Http.GetJsonAsync<ToDoList[]>("/api/ToDo");
    StateHasChanged();
    }
    private async Task AddToDo()
    {
    if (!string.IsNullOrEmpty(itemName))
    {
    await Http.SendJsonAsync(HttpMethod.Post, "/api/ToDo", new ToDoList
    {
    Item = itemName,
    });
    itemName = "";
    await Refresh();
    }
    }
    private async Task UpdateToDo(int id)
    {
    if (!string.IsNullOrEmpty(UpdateItemName))
    {
    await Http.SendJsonAsync(HttpMethod.Put, $"/api/ToDo/{id}", new ToDoList
    {
    ID = id,
    Item = UpdateItemName,
    });
    await Refresh();
    //UpdateItemName = "";
    RegisteredFunction.Invoke<bool>("ShowControl", id.ToString(), "", false);
    }
    }
    private async Task DeleteToDo(int id)
    {
    await Http.DeleteAsync($"/api/ToDo/{id}");
    await Refresh();
    }
    private void EditToDo(int id, string itemName)
    {
    RegisteredFunction.Invoke<bool>("ShowControl", id.ToString(), itemName, true);
    }
    private void CancelToDo(int id)
    {
    RegisteredFunction.Invoke<bool>("ShowControl", id.ToString(), "", false);
    }
    private async Task DeleteAllToDos()
    {
    foreach (var c in todos)
    {
    await Http.DeleteAsync($"/api/ToDo/{c.ID}");
    }
    await Refresh();
    }
    }
    view raw ToDo.html hosted with ❤ by GitHub
    Let’s understand this code. The above code can be divided into 3 sections (HTML, JavaScript and C#). We’ll take a look at each of them in details. before, we do that, let’s understand the very first 4 lines.

    1
    2
    3
    4
    @page "/todo"
    @using ASPNETBlazorCRUDApp.Shared
    @using Microsoft.AspNetCore.Blazor.Browser.Interop
    @inject HttpClient Http
    • We are defining the route for this page using @page directive. So appending “/todo” to base URL will redirect to this component.
    • Including the ASPNETBlazorCRUDApp.Shared namespace as ToDoList class is used.
    • The namespace Microsoft.AspNetCore.Blazor.Browser.Interop is included to call JavaScript from the Blazor code. Currently, WebAssembly and therefore Mono and Blazor have no direct access to the browser’s DOM API. Read this post for a detailed information.
    • We are also injecting HTTPClient dependency to call the server-side API.

    Now,let’s understand the each section.

    • HTML

      The HTML part is quite simple if you are familiar with Razor syntax. It has a table and a couple of buttons. The @bind is used for two-way data-binding. Read more about data binding here. The @onclick is used to call the method on the click of the button.

    • JavaScript

      Blazor directly can’t call JavaScript function. From learn-blazor.com

      Blazor is built upon Mono and WebAssembly (short Wasm). Currently, WebAssembly and therefore Mono and Blazor have no direct access to the browser’s DOM API. But given that Blazor is a frontend framework, it needs to access the DOM to build the user interface. Additionally, as web developers we want to access browser APIs and existing JavaScript functions, too.

      WebAssembly programs can call JavaScript functions and those in turn can use all available browser APIs. Because Mono is compiled to WebAssembly and Blazor runs on Mono, Blazor can indirectly call any JavaScript functions with the aid of Mono.

      Steve Sanderson mentioned in the blazor intro post,

      To call other JS libraries or your own custom JS/TS code from .NET, the current approach is to register a named function in a JavaScript/TypeScript file, e.g.:

      1
      2
      3
      4
      // This is JavaScript
      Blazor.registerFunction('doPrompt', message => {
        return prompt(message);
      });

      … and then wrap it for calls from .NET:

      1
      2
      3
      4
      5
      // This is C#
      public static bool DoPrompt(string message)
      {
          return RegisteredFunction.Invoke<bool>("doPrompt", message);
      }

      The JavaScript function named ShowControl defined in the code, controls the visibility of the HTML elements. This is called from C# code on click of Edit, Update and Cancel button.

    • C#

      At the bottom of the page, there is a @functions section which contains code for making server-side web api call for CRUD operations, calling JavaScript and refreshing the ToDo list. The only new thing here is the way C# makes a call to JavaScript ShowControl function on Edit, Update and Cancel button. Other than that it’s simple and familiar code.

  • Lastly, we need to add the “ToDo” component link in the navigation bar. To do that, open the NavMenu.html file located inside the Shared folder in the client app and add the following code just after the “Fetch Data” link.
    <li>
    <NavLink href="/todo">
    <span class='glyphicon glyphicon-list-alt'></span> To Do
    </NavLink>
    </li>
  • That’s it. Run the application and you should see the following.

    ASP.NET Blazor Crud Operation

    The source code is available at GitHub. Feel free to play around with this app to get a feel of Blazor. There are a few bugs in the app and I am still working on fixing them. I faced a lot of issues while creating this simple app. But it’s a decent start and still early days for Blazor. It’s easy to begin with it, but difficult to make progress due to the following reasons,

    • Blazor itself is in the experimental phase
    • No official and detailed documentation.
    • Not enough help available online.

    Update: Here is an updated solution to this app which removes the JavaScript dependency.

    Final Thoughts

    These are still early days for Blazor, but we can start building component-based web apps that run in the browser with .NET. Initially, you would find difficult to make progress. Microsoft has promised to bring many new features and improvements planned for future updates. Let’s hope that soon there will be an official documentation and great community support. In this post, we learned about the new .NET framework – Blazor and also created a simple CRUD application using Blazor.

    I am really excited to play with this, so more blog post coming up on Blazor. Thank you for reading. Keep visiting this blog and share this in your network. Please put your thoughts and feedback in the comments section.

    PS: If you found this content valuable and want to return the favour, then Buy Me A Coffee

    22 thoughts to “Create a CRUD App using Blazor and ASP.NET Core”

    1. Blazor.registerFunction(‘ShowControl’, (id, item, bShow) => {

      return true;
      });

      how to mention js function name only with registerFunction and define function in other js file?
      Blazor.registerFunction(‘ShowControl’, (id, item, bShow))

      function ShowControl(id, item, bShow)
      {

      }

      share the knowledge
      Thanks

    2. This is neat!

      I’ll be implementing this once I get past the current problem I’m having.

      I’m attempting to pull data from an SQL databse, I’ve got my Blazor demo set up, API’s created and sending a get request /api/sendmedata gives me back valid json. However, when copying the fetchdata example it doesn’t seem to update my list and just stays stuck at “loading…” As you said, not a lot of support for Blazor right now, so it’s a tough pickle to crack.

          1. I got the problem. You are making a call to GetByID API which returns a single object of VisitorLogTable and you are trying to store it in a list, which is causing the problem.

            This call is working fine as the Get method returns a list.
            visitors = await Http.GetJsonAsync(“/api/VisitorLogTables”);

            1. I’m using chrome and firefox, as chrome’s error reporting for mono is a little… well it’s not very good. Could be something wrong with my chrome, so I’ve turned to firefox which is at least giving me console errors.

              Here’s a few notes on the issue I’m seeing:
              This returns a list of 3000 items, I’m not sure if that’s what’s causing the error I’m seeing, but when not querying a specific ID, I get “index out of bounds”, if I navigate directly to the api page in my browser, it just gives me all the data, all 3556 items. Could that number be what’s causing the “index out of bounds” error?

              When I run it with just the 11, i get the “Number of parameters specified does not match the expected number.”, which I think is as you’ve pinned it, because it’s expecting more items in my list.

              How would I go about paring down the items returned so it only returns a subset of data?

            2. How about implementing “Paging”? Thinking about Paging implementation with blazor scares me. 😉

              But, before that I would suggest you to manipulate your database to return only 10-15 records to verify the code is working fine. If the problem is with respect to number of records then you can think about implementing paging.. Tough task though 🙂

            3. So here’s what I’ve done, I’ve pruned the database down to 11 items, and on init gives me 400 (Bad Request).

              I’ve added a button that perform the call and it gives me back this in the console…
              WASM: Mono: Warning: Degraded allocation. Consider increasing nursery-size if the warning persists.
              WASM: OnInit callBlazorDemo.Shared.VisitorLogTable[]
              WASM: Mono: GC_MINOR: (Nursery full) time 0.00ms, stw 0.00ms promoted 171K major size: 1376K in use: 704K los size: 1024K in use: 151K

              I did update the code slightly to follow the recommended approach from the learn-blazor website for consuming API.

              private VisitorLogTable[] Visitors;

              protected override async Task OnInitAsync()
              {
              await RefreshCustomerList();
              }

              private async Task RefreshCustomerList()
              {
              Visitors = await Http.GetJsonAsync(“/api/VisitorLogTables”);
              StateHasChanged();
              }

              private async Task PrintWebApiResponse()
              {
              var Visitors = await Http.GetJsonAsync(“/api/VisitorLogTables”);
              //Console.WriteLine(“api call” +response);
              Console.WriteLine(“OnInit call” + Visitors);
              StateHasChanged();
              }
              Each time, navigating to the APi gives me back the json object (localhost/api/visitorlogtables) returns the json perfectly.

              I gotta tell you, this is a real head-scratcher for me. I feel like everything should be running, not quite sure what the bottleneck is here.

              I do have other fields I’m not using, about 30 fields total when I’m only trying to allocate 4 of them to this page, but I don’t think that’s the issue. I tried updating the loop to include all the fields yesterday and had the same result.

              I reverted the code back to what I had posted in the github, same exact results. It doesn’t like this query for some reason, but it works just find when done manually, very confused here.

    Leave a Reply

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