You have developed your Asp.Net Core MVC, it’s working fine and then you want to take it into the next level: to make it installable and allow it to be distributed in the platform store. But a web app is a web app and, as so, it should run in the browser. That would be true some years ago, but not anymore.

There are several ways to transform your web app into an installable app, without rewriting it. One easy way to do it is to transform it into a Progressive Web App (PWA). A PWA is an app that allows to be installed in the hosting OS, is multiplatform, and doesn’t need the browser to be ran. That seems too good to be true, but there is more: it can also use the resources of the hosting OS, as a native app and can be sent and distributed in the platform store.

That seems very nice, no ? And what are the requirements to make a PWA ?

  • It should be launchable and installable
  • It should be fast, even on slow networks
  • It should serve data using HTTPS, not HTTP
  • It should offer offline experience
  • It should run in all browsers
  • It should have responsive design

The full checklist for a PWA can be found here. There is a resource to audit your app, it’s called Lighthouse and can be accessed by opening the Dev Tools (F12):

If you click on Generate report, it will scan your website to see if you have everything to run your app as a PWA:

Ok. Now we know how to check if our website is ready for being a PWA, but what does it take to transform it into a PWA? Basically, two things: a Service Worker and a manifest file.

The Service worker is a script that runs separately from your web page and offers extra functionality, like push notifications and background sync. You can read more about it here.

The app manifest is a json file that will add metadata information for your app. in a way that it can be used to install the app. Sound simple, no ? Well, it’s not that difficult, we’ll do it right now.

The first step is to create a service worker, we’ll use the tutorial described in  https://developers.google.com/web/tools/workbox/guides/get-started. Let’s create a file named serviceWorker.js and add it to the wwwroot folder:

console.log('Hello from serviceWorker.js');

Then, we’ll register it in the end of the _Layout.cshtml file:

<script>
    // Check that service workers are supported
    if ('serviceWorker' in navigator) {
        // Use the window load event to keep the page load performant
        window.addEventListener('load', () => {
            navigator.serviceWorker.register('/serviceWorker.js');
        });
    }
</script>

Once you do that, you can open the dev tools and, in the console, you will see the message:

You can also check in the Application tab, under Service Workers, that it’s up and running:

This works, but it’s not very useful. You can create your own service worker by using Workbox, a tool developed by Google, to develop something that matches your needs. For now, we’ll use workbox cli to generate the service worker (for that, you will have to have npm installed in your machine).  Install the workbox cli with:

npm install workbox-cli --global

Then run the wizard, to generate a configuration file:

workbox wizard

Once the wizard has ran, you can generate the service worker with:

workbox generateSW workbox-config.js

That will generate a sw.js file in wwwroot. You have to change the register procedure in _Layout.cshtml to reflect this change:

<script>
    // Check that service workers are supported
    if ('serviceWorker' in navigator) {
        // Use the window load event to keep the page load performant
        window.addEventListener('load', () => {
            navigator.serviceWorker.register('/sw.js');
        });
    }
</script>

 

Once you do that, the next step is to add a manifest file to your app. The easiest way to do it is to use a manifest generator, like this one:

You will need an icon for the app. I went to https://freeicons.io and downloaded an image for my icon and added it to the manifest generator. Then I downloaded a zip with the manifest file and the icons for the app. The manifest file is something like this:

{
    "theme_color": "#3558f6",
    "background_color": "#2a10b1",
    "display": "standalone",
    "scope": "/",
    "start_url": "/",
    "name": "MyMVCApp",
    "short_name": "mymvcapp",
    "description": "Sample MVC PWA app",
    "icons": [
        {
            "src": "/images/icon-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "/images/icon-256x256.png",
            "sizes": "256x256",
            "type": "image/png"
        },
        {
            "src": "/images/icon-384x384.png",
            "sizes": "384x384",
            "type": "image/png"
        },
        {
            "src": "/images/icon-512x512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ]
}

This manifest file must be put on the wwwroot folder of the app with the name of manifest.json and the images, on wwwroot/images folder.

Your PWA is ready. You can run it and see if it’s installable, by checking if this icon is present in Edge (there should be a similar one in the other browsers):

Clicking on it, you will have an install prompt:

If you choose to install it, you will have an app that will run in a window, will have an icon (which you can pin in the taskbar in Windows) and an icon in the start menu:

This app is multi-platform, meaning that you can install it on Linux, Mac or any phone platform. If you press F12 to open the Dev Tools, they will open and then, in the Application tab, you can see the manifest data:

We can check the app in Lighthouse for the PWA compatibility:

From it, you can see three errors

  • Redirects HTTP traffic to HTTPS Error!
  • Does not provide a valid apple-touch-icon
  • Manifest doesn’t have a maskable icon
If you see the details, you can see they are easy to fix:
The second one needs just adding a non transparent icon to the images folder and add this line in the head section of _Layout.cshtml:
<link rel="apple-touch-icon" href="/images/apple-touch-icon.png">

This will provide a valid apple-touch-icon

The third is just provide a maskable icon for Android devices. This is done adding a purpose in the icon on the manifest:

"icons": [
  {
    "src": "/images/icon-192x192.png",
    "sizes": "192x192",
    "type": "image/png",
    "purpose" :  "maskable" 
  },

The first error is already fixed: on the Startup.cs, we already have this:

app.UseHttpsRedirection();

This will allow redirection from http to https, but we are using a port, and we cannot use the same port for http and https (every protocol must have its own port). To test it, we should have it stored in a web site and use the default ports (80 for http and 443 for https). That way we wouldn’t need to use the ports in the address and the test could be done with no problems. If we have IIS installed in our machine, then the problem is solved.

If we don’t have it, we still have an alternative: launch the app in the correct ports. One way to do it is to use the dotnet cli and launch Kestrel with the correct ports. This is done with this command line:

dotnet run --urls "http://localhost:80;https://localhost:443"

It will lauch the app with the default ports (that won’t run if you already have some web server listening in those ports), so we can lauch our app with http://localhost or https://localhost.

Once you make these changes and run Lighthouse again, you will get:

Now the app passes the PWA test. But this is not everything that a PWA can do. It can interact with the OS, like a native app, and we will see how to do it.

The first thing that can be done is to add a shortcut to the taskbar. This is done in the manifest. by adding a Shortcuts clause. We will add a shortcut for the privacy policy:

"shortcuts": [
  {
    "name": "Privacy policy",
    "short_name": "Privacy",
    "description": "View the privacy policy",
    "url": "/Home/Privacy",
    "icons": [
      {
        "src": "/images/icon-lock.png",
        "sizes": "128x128"
      }
    ]
  }
]

You will need to add an icon in the images folder, named icon-lock.png. Once you add these and reinstall the app, when you right click in the taskbar icon, you will see a new menu option:

If you click in this new menu option, it will open a new window with the privacy policy.

One other thing that you can do with PWAs is to access the file system. In the Views\Home folder, create a new FileSystem.cshtml file:

@{
    ViewData["Title"] = "File System";
}
<div class="container">
    <pre id="textBox" class="pre-scrollable" contenteditable="true">Click on the button to load a file</pre>
    <button class="btn btn-secondary" onclick="loadFile()">Load file</button>
    <button class="btn btn-secondary" onclick="saveFile()">Save file</button>
</div>
<script>
    loadFile = async () => {
        const [handle] = await window.showOpenFilePicker();
        const file = await handle.getFile();
        const text = await file.text();
        $("#textBox").text(text);
    }
    saveFile = async () => {
        const options = {
            types: [
                {
                    description: 'Text Files',
                    accept: {
                        'text/plain': ['.txt'],
                    },
                },
            ],
        };
        const handle = await window.showSaveFilePicker(options);
        const writable = await handle.createWritable();
        await writable.write($("#textBox").text());
        await writable.close();
    }

</script>

This file will show a text area with two buttons, for load and save a file. The loadFile function will open a OpenFilePicker, get a file handle, open the file and get its text and load the text area. The saveFile function will open a SaveFilePicker, create a writable file and save the textarea text to it. The next step is to add the link to this page in the main menu. In _Layout.cshtml, add this between the Home and the Privacy links:

<li class="nav-item">
    <a class="nav-link text-dark" asp-area="" asp-controller="Home" 
      asp-action="FileSystem">File System</a>
</li>

When you run the project, you will have a new option that will take you to the file system page:

Now, you have the option to open and save the files to your file system.

Another thing that can be done is to share data with other apps. In the File System page, add a new button:

<button class="btn btn-secondary" onclick="shareData()">Share</button>

The code for the button is:

shareData = async () => {
    const data = {
        title: 'Sharing data with PWA',
        text: $("#textBox").text(),
        url: 'https://localhost:5001',
    }
    try {
        await navigator.share(data)
    } catch (err) {
        alert('Error: ' + err);
    }
}

This code will load the text in the text area and will share it with the registered apps. When you run this app and click the Share button, you will see something like this:

 

 

If you choose the Mail application, it will open the mail app with the text data:

One other feature that can be enabled is to run the app as a tabbed app. For that, the only thing to do is to enable that, opening the url edge://flags and enabling Desktop PWA tab strips:

When you run the installed app, it will open as a tabbed window:

These are some features available for the PWAs, and more are being added every day. You can check some experimental features in Edge here

All the source code for this project is at https://github.com/bsonnino/MvcPwa

One criticism about Asp.Net web api is the ceremony to create a simple web api. This criticism has some reason: when you create a bare-bones web api with this command:

dotnet new webapi

You will get something like this:

As you can see, there is a lot of plumbing going on in Program.cs and Startup.cs and the real meat is in Controllers\WeatherForecastController.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace WebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

Even that has a lot going on: the ApiControllerRoute and HttpGet attributes, besides the code (well, that’s needed in any platform, anyway :-)). For comparison, we could create the same web api using node.js with Express with this code:

const express = require("express");

const app = express();

const nextElement = (n) => Math.floor(Math.random() * n);

const summaries = [
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];

app.get("/weatherforecast", (req, res, next) => {

    const forecasts = [...Array(5).keys()].map(i => (
        {
            "date": new Date() + i,
            "temperatureC": nextElement(75) - 20,
            "summary": summaries[nextElement(summaries.length)]
        }));
    
    res.json(forecasts);
});

app.listen(3000, () => {
    console.log("Server running on port 3000");
});

 

As you can see, there is a lot less code for the same api. The Express library takes care of setting up the server, listening to the port and mapping the route. The response for the request is the same as the one generated by the Asp.Net api.

To fix this issue, David Fowler, a Microsoft software architect designed a way to create Asp.Net web apis with low code. In fact, you can do it with a comparable amount of code than with Node. It’s called FeatherHTTP and it’s available here.

To use it, you must install the template and you can use it with dotnet new.

To install the template you can use this command:

dotnet new -i FeatherHttp.Templates::0.1.83-alpha.g15473de7d1 --nuget-source https://f.feedz.io/featherhttp/framework/nuget/index.json

When you do it, the option feather appears in dotnet new:

 

You can now create a new project with the command:

dotnet new feather -n FeatherApi

When the project is created, you can see that it’s much lighter than the standard code: there is only the csproj file and Program.cs with this code:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

var app = WebApplication.Create(args);

app.MapGet("/", async http =>
{
    await http.Response.WriteAsync("Hello World!");
});

await app.RunAsync();

Much smaller, no? As you can see, it uses the new C#9 feature “Top level statements” to reduce a little bit the code. We can change the code to have the same output of the standard code:

using System;
using System.Linq;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

var app = WebApplication.Create(args);
var Summaries = new[]
{
   "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", async http =>
{
    var rng = new System.Random();
    var weatherForecast = Enumerable.Range(1, 5)
        .Select(index => new WeatherForecast(
            DateTime.Now.AddDays(index), rng.Next(-20, 55), 
            Summaries[rng.Next(Summaries.Length)])).ToArray();
    http.Response.ContentType = "application/json";    
    await http.Response.WriteAsync(JsonSerializer.Serialize(weatherForecast));
});

await app.RunAsync();

public record WeatherForecast(DateTime Date, int TemperatureC,string Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

Now we have something comparable to Node! I’ve used here another feature of C#9, Records, to reduce a little the code size.

As we can see, with FeatherHttp, we can reduce the code size for an Asp.Net web api app, but can we still use it for a more complex API ?

We’ll try to reproduce the code used in my HttpRepl article here.

The first step is to copy the Customer class to the project. We’ll convert it to a record:

public record Customer(string CustomerId, string CompanyName,
  string ContactName, string ContactTitle, string Address,
  string City, string Region, string PostalCode, string Country,
  string Phone, string Fax);

Then we’ll copy the repository:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;

namespace CustomerApi.Model
{
    public class CustomerRepository
    {
        private readonly IList<Customer> customers;

        public CustomerRepository()
        {
            var doc = XDocument.Load("Customers.xml");
            customers = new ObservableCollection<Customer>(
                doc.Descendants("Customer").Select(c => new Customer(
                    GetValueOrDefault(c, "CustomerID"), GetValueOrDefault(c, "CompanyName"),
                    GetValueOrDefault(c, "ContactName"), GetValueOrDefault(c, "ContactTitle"),
                    GetValueOrDefault(c, "Address"), GetValueOrDefault(c, "City"),
                    GetValueOrDefault(c, "Region"), GetValueOrDefault(c, "PostalCode"),
                    GetValueOrDefault(c, "Country"), GetValueOrDefault(c, "Phone"),
                    GetValueOrDefault(c, "Fax")
            )).ToList());
        }

        #region ICustomerRepository Members

        public bool Add(Customer customer)
        {
            if (customers.FirstOrDefault(c => c.CustomerId == customer.CustomerId) == null)
            {
                customers.Add(customer);
                return true;
            }
            return false;
        }

        public bool Remove(Customer customer)
        {
            if (customers.IndexOf(customer) >= 0)
            {
                customers.Remove(customer);
                return true;
            }
            return false;
        }

        public bool Update(Customer customer)
        {
            var currentCustomer = GetCustomer(customer.CustomerId);
            if (currentCustomer == null)
                return false;
            var index = customers.IndexOf(currentCustomer);
            currentCustomer = new Customer(customer.CustomerId, customer.CompanyName,
              customer.ContactName, customer.ContactTitle, customer.Address, customer.City,
              customer.Region, customer.PostalCode, customer.Country, customer.Phone, customer.Fax);
            customers[index] = currentCustomer;
            return true;
        }

        public bool Commit()
        {
            try
            {
                var doc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
                var root = new XElement("Customers");
                foreach (Customer customer in customers)
                {
                    root.Add(new XElement("Customer",
                                          new XElement("CustomerID", customer.CustomerId),
                                          new XElement("CompanyName", customer.CompanyName),
                                          new XElement("ContactName", customer.ContactName),
                                          new XElement("ContactTitle", customer.ContactTitle),
                                          new XElement("Address", customer.Address),
                                          new XElement("City", customer.City),
                                          new XElement("Region", customer.Region),
                                          new XElement("PostalCode", customer.PostalCode),
                                          new XElement("Country", customer.Country),
                                          new XElement("Phone", customer.Phone),
                                          new XElement("Fax", customer.Fax)
                                 ));
                }
                doc.Add(root);
                doc.Save("Customers.xml");
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        public IEnumerable<Customer> GetAll() => customers;

        public Customer GetCustomer(string id) => customers.FirstOrDefault(c => string.Equals(c.CustomerId, id, StringComparison.CurrentCultureIgnoreCase));

        #endregion

        private static string GetValueOrDefault(XContainer el, string propertyName)
        {
            return el.Element(propertyName) == null ? string.Empty : el.Element(propertyName).Value;
        }
    }
}

One note here: as we declared Customer as a Record, it’s immutable, so we cannot simply replace its data with the new data in the Update method. Instead, we create a new customer and replace the current one with the newly created in the customers list.

We also need the customers.xml file that can be obtained at https://github.com/bsonnino/HttpRepl/blob/main/Customers.xml and add it to the project by adding this clause in the csproj file:

<ItemGroup >
  <None Update="customers.xml" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

Now, we will change the main program to create the API:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using CustomerApi.Model;
using System.Text.Json;
using System.Threading.Tasks;

var app = WebApplication.Create(args);

app.MapGet("/", GetAllCustomers);
app.MapGet("/{id}", GetCustomer);
app.MapPost("/", AddCustomer);
app.MapPut("/", UpdateCustomer);
app.MapDelete("/{id}", DeleteCustomer);

await app.RunAsync();

async Task GetAllCustomers(HttpContext http)
{
    var customerRepository = new CustomerRepository();
    http.Response.ContentType = "application/json";
    await http.Response.WriteAsync(JsonSerializer.Serialize(customerRepository.GetAll()));
}

async Task GetCustomer(HttpContext http)
{
    if (!http.Request.RouteValues.TryGet("id", out string id))
    {
        http.Response.StatusCode = 400;
        return;
    }
    var customerRepository = new CustomerRepository();
    Customer customer = customerRepository.GetCustomer(id);
    if (customer != null)
    {
        http.Response.ContentType = "application/json";
        await http.Response.WriteAsync(JsonSerializer.Serialize(customer));
    }
    else
        http.Response.StatusCode = 404;
    return;
}

async Task AddCustomer(HttpContext http)
{
    var customer = await http.Request.ReadFromJsonAsync<Customer>();
    if (string.IsNullOrWhiteSpace(customer.CustomerId))
    {
        http.Response.StatusCode = 400;
        return;
    }
    var customerRepository = new CustomerRepository();
    if (customerRepository.Add(customer))
    {
        customerRepository.Commit();
        http.Response.StatusCode = 201;
        http.Response.ContentType = "application/json";
        await http.Response.WriteAsync(JsonSerializer.Serialize(customer));
        return;
    }
    http.Response.StatusCode = 409;
    return;
}

async Task UpdateCustomer(HttpContext http)
{
    var customer = await http.Request.ReadFromJsonAsync<Customer>();
    if (string.IsNullOrWhiteSpace(customer.CustomerId))
    {
        http.Response.StatusCode = 400;
        return;
    }
    var customerRepository = new CustomerRepository();
    var currentCustomer = customerRepository.GetCustomer(customer.CustomerId);
    if (currentCustomer == null)
    {
        http.Response.StatusCode = 404;
        return;
    }
    if (customerRepository.Update(customer))
    {
        customerRepository.Commit();
        http.Response.ContentType = "application/json";
        await http.Response.WriteAsync(JsonSerializer.Serialize(customer));
        return;
    }
    http.Response.StatusCode = 204;
    return;
}


async Task DeleteCustomer(HttpContext http)
{
    if (!http.Request.RouteValues.TryGet("id", out string id))
    {
        http.Response.StatusCode = 400;
        return;
    }
    var customerRepository = new CustomerRepository();
    var currentCustomer = customerRepository.GetCustomer(id);
    if (currentCustomer == null)
    {
        http.Response.StatusCode = 404;
        return;
    }
    if (customerRepository.Remove(currentCustomer))
    {
        customerRepository.Commit();
        http.Response.StatusCode = 200;
        return;
    }
    http.Response.StatusCode = 204;
    return;
}

We are mapping the routes for all the desired actions. For each mapping, we’ve created a method to use when the corresponding route is being called:

  • For GetAll, we ar using the same method we’ve used with the WeatherForecast, getting all customers, serializing the array into a json string and writing the response
  • To get just one customer, where the id is in the route, we get the id with http.Request.RouteValues.TryGet, get the customer and write the response
  • To add and update the customer, we must get it from the body with await http.Request.ReadFromJsonAsync<Customer>(); Once we get it, we can add or update the customer repository
  • Delete is similar to getting just one customer, but we delete it from the repository, instead of returning it.

You can test the API by using the same methods we’ve used in the HttpRepl article, the API will be the same, but in this case, I’m not using the customers route. Another minor issue is that Swagger is still not available in FeatherHttp (https://github.com/featherhttp/framework/issues/34), but this doesn’t change the functionality at all.

FeatherHttp is not production ready, it’s still in alpha, but it shows a direction to lightweight Asp.Net web api applications. As you can see, all the ceremony from Asp.Net web api can be removed and you can finish with an app as small as a Node one, but written in C#.

All the source code for this article is at https://github.com/bsonnino/FeatherHttp

 

 

 

 

With .NET 5.0, two small features were introduced to Asp.NET and were almost unnoticed: Open Api and HTTPRepl. Open Api is not something new, it’s been available for a long time, but it had to be included explicitly in a new project. Now, when you create a new project, it’s automatically included in the project and you can get the Api documentation using Swagger.

Now, when you create a new project with

dotnet new webapi

You will create a new WebApi project with a Controller, WeatherController, that shows 10 values of a weather forecast:

It’s a simple app, but it already comes with the OpenApi (Swagger) for documentation. Once you type the address:

https://localhost:5001/swagger

You will get the Swagger documentation and will be able to test the service:

But there is more. Microsoft introduced also HttpRepl, a REPL (Read-Eval-Print Loop) for testing REST services. It will scan your service and allow you to test the service using simple commands, like the file commands.

To test this new feature, in  new folder create a webapi app with

dotnet new webapi

Then, open Visual Studio Code with

code .

You will get something like that:

Then, delete the WeatherForecast.cs file and add a new folder and name it Model. In it, add a new file and name it Customer.cs and add this code:

public class Customer
{
    public string CustomerId { get; set; }
    public string CompanyName { get; set; }
    public string ContactName { get; set; }
    public string ContactTitle { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string Region { get; set; }
    public string PostalCode { get; set; }
    public string Country { get; set; }
    public string Phone { get; set; }
    public string Fax { get; set; }
}

Create a new file and name it CustomerRepository.cs and add this code:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;

namespace HttpRepl.Model
{
    public class CustomerRepository
    {
        private readonly IList<Customer> customers;

        public CustomerRepository()
        {
            var doc = XDocument.Load("Customers.xml");
            customers = new ObservableCollection<Customer>((from c in doc.Descendants("Customer")
                                                            select new Customer
                                                            {
                                                                CustomerId = GetValueOrDefault(c, "CustomerID"),
                                                                CompanyName = GetValueOrDefault(c, "CompanyName"),
                                                                ContactName = GetValueOrDefault(c, "ContactName"),
                                                                ContactTitle = GetValueOrDefault(c, "ContactTitle"),
                                                                Address = GetValueOrDefault(c, "Address"),
                                                                City = GetValueOrDefault(c, "City"),
                                                                Region = GetValueOrDefault(c, "Region"),
                                                                PostalCode = GetValueOrDefault(c, "PostalCode"),
                                                                Country = GetValueOrDefault(c, "Country"),
                                                                Phone = GetValueOrDefault(c, "Phone"),
                                                                Fax = GetValueOrDefault(c, "Fax")
                                                            }).ToList());
        }

        #region ICustomerRepository Members

        public bool Add(Customer customer)
        {
            if (customers.FirstOrDefault(c => c.CustomerId == customer.CustomerId) == null)
            {
                customers.Add(customer);
                return true;
            }
            return false;
        }

        public bool Remove(Customer customer)
        {
            if (customers.IndexOf(customer) >= 0)
            {
                customers.Remove(customer);
                return true;
            }
            return false;
        }

        public bool Update(Customer customer)
        {
            var currentCustomer = GetCustomer(customer.CustomerId);
            if (currentCustomer == null)
                return false;
            currentCustomer.CustomerId = customer.CustomerId;
            currentCustomer.CompanyName = customer.CompanyName;
            currentCustomer.ContactName = customer.ContactName;
            currentCustomer.ContactTitle = customer.ContactTitle;
            currentCustomer.Address = customer.Address;
            currentCustomer.City = customer.City;
            currentCustomer.Region = customer.Region;
            currentCustomer.PostalCode = customer.PostalCode;
            currentCustomer.Country = customer.Country;
            currentCustomer.Phone = customer.Phone;
            currentCustomer.Fax = customer.Fax;
            return true;    
        }

        public bool Commit()
        {
            try
            {
                var doc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
                var root = new XElement("Customers");
                foreach (Customer customer in customers)
                {
                    root.Add(new XElement("Customer",
                                          new XElement("CustomerID", customer.CustomerId),
                                          new XElement("CompanyName", customer.CompanyName),
                                          new XElement("ContactName", customer.ContactName),
                                          new XElement("ContactTitle", customer.ContactTitle),
                                          new XElement("Address", customer.Address),
                                          new XElement("City", customer.City),
                                          new XElement("Region", customer.Region),
                                          new XElement("PostalCode", customer.PostalCode),
                                          new XElement("Country", customer.Country),
                                          new XElement("Phone", customer.Phone),
                                          new XElement("Fax", customer.Fax)
                                 ));
                }
                doc.Add(root);
                doc.Save("Customers.xml");
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        public IEnumerable<Customer> GetAll() => customers;

        public Customer GetCustomer(string id) => customers.FirstOrDefault(c => string.Equals(c.CustomerId, id, StringComparison.CurrentCultureIgnoreCase));

        #endregion

        private static string GetValueOrDefault(XContainer el, string propertyName)
        {
            return el.Element(propertyName) == null ? string.Empty : el.Element(propertyName).Value;
        }
    }
}

This code will use a file, named Customers.xml and will use it to serve the customers repository. With it, you will be able to get all customers, get, add, update or delete one customer. We will use it to serve our controller. The Customers.xml can be obtained at . You should add this file to the main folder and then add this code:

<ItemGroup >
  <None Update="customers.xml" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

This will ensure that the xml file is copied to the output folder when the project is built.

Then, in the Controllers folder, delete the WeatherForecastController and add a new file, CustomerController.cs and add this code:

using System.Collections.Generic;
using HttpRepl.Model;
using Microsoft.AspNetCore.Mvc;

namespace HttpRepl.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class CustomerController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<Customer> Get()
        {
            var customerRepository = new CustomerRepository();
            return customerRepository.GetAll();
        }

        [HttpGet("{id}")]
        public IActionResult GetCustomer(string id)
        {
            var customerRepository = new CustomerRepository();
            Customer customer = customerRepository.GetCustomer(id);
            return customer != null ? Ok(customer) : NotFound();
        }

        [HttpPost]
        public IActionResult Add([FromBody] Customer customer)
        {
            if (string.IsNullOrWhiteSpace(customer.CustomerId))
              return BadRequest();
            var customerRepository = new CustomerRepository();
            if (customerRepository.Add(customer))
            {
                customerRepository.Commit();
                return CreatedAtAction(nameof(Get), new { id = customer.CustomerId }, customer);
            }
            return Conflict();
        }

        [HttpPut]
        public IActionResult Update([FromBody] Customer customer)
        {
            if (string.IsNullOrWhiteSpace(customer.CustomerId))
              return BadRequest();
            var customerRepository = new CustomerRepository();
            var currentCustomer = customerRepository.GetCustomer(customer.CustomerId);
            if (currentCustomer == null)
                return NotFound();
            if (customerRepository.Update(customer))
            {
                customerRepository.Commit();
                return Ok(customer);
            }
            return NoContent();
        }

        [HttpDelete("{id}")]
        public IActionResult Delete([FromRoute]string id)
        {
            if (string.IsNullOrWhiteSpace(id))
              return BadRequest();
            var customerRepository = new CustomerRepository();
            var currentCustomer = customerRepository.GetCustomer(id);
            if (currentCustomer == null)
                return NotFound();
            if (customerRepository.Remove(currentCustomer))
            {
                customerRepository.Commit();
                return Ok();
            }
            return NoContent();
        }
    }
}

This controller will add actions to get all customers, get add, update or delete one customer. The project is ready to be run. You can run it with dotnet run and, when you open a new browser window and type this address:

https://localhost:5001/swagger

You will get something like this:

You can test the service with the Swagger page (as you can see, it was generated automatically wen you compiled and ran the app), but there is still another tool: HttpRepl. This tool was added with .NET 5 and you can install it with the command:

dotnet tool install -g Microsoft.dotnet-httprepl

Once you install it, you can run it with

httprepl https://localhost:5001

When you run it, you will get the REPL prompt:

If you type the uicommand, a new browser window will open with the Swagger page. You can type lsto list the available controllers and actions:

As you can see, it has a folder structure and you can test the service using the command line. For example, you can get all the customers with get customer or get one customer with get customer/blonp:

You can also “change directory” to the Customer “directory” with cd Customer. In this case, you can query the customer with get blonp:

If the body content is simple, you can use the -c parameter, like in:

post -c "{"customerid":"test"}"

This will add a new record with just the customer id:

If the content is more complicated, you must set the default editor, so you can edit the customer that will be inserted in the repository. You must do that with the command:

pref set editor.command.default "c:\windows\system32\notepad.exe"

This will open notepad when you type a command that needs a body, so you can type the body that will be sent when the command is executed. If you type post in the command line, notepad will open to edit the data. You can type this data:

{
  "customerId": "ABCD",
  "companyName": "Abcd Inc.",
  "contactName": "A.B.C.Dinc",
  "contactTitle": "Owner",
  "address": "1234 - Acme St - Suite A",
  "city": "Abcd",
  "region": "AC",
  "postalCode": "12345",
  "country": "USA",
  "phone": "(501) 555-1234"
}

When you save the file and close notepad, you will get something like this:

If you try to add the same record again, you will get an error 409, indicating that the record already exists in the database:

As you can see, the repository is doing the checks and sending the correct response to the REPL. You can use the same procedure to update or delete a customer. For the delete, you just have to pass the customer Id:

Now that we know all the commands we can do with the REPL, we can go one step further: using scripts. You can write a text file with the commands to use and run the script. Let’s say we want to exercise the entire API, by issuing all commands in a single run. We can create a script like this (we’ll name it TestApi.txt)

connect https://localhost:5001
ls
cd Customer
ls
get 
get alfki
post --content "{"customerId": "ABCD","companyName": "Abcd Inc.","contactName": "A.B.C.Dinc"}"
get abcd
put --content "{"customerId": "ABCD","companyName": "ACME Inc"}"
delete abcd
get abcd
cd ..
ls

And then open HttpRepl and run the command

run testapi.txt

We’ll get this output:

As you can see, with this tool, you get a very easy way to exercise your API. It’s not something fancy as a dedicated program like Postman, but it’s easy to use and does its job.

The full code for the project is at https://github.com/bsonnino/HttpRepl

 

One of the perks of being an MVP is to receive some tools for my own use, so I can evaluate them and if, I find them valuable, I can use them on a daily basis. On my work as an architect/consultant, one task that I often find is to analyse an application and suggest changes to make it more robust and maintainable. One tool that can help me in this task is NDepend (https://www.ndepend.com/). With this tool, you can analyse your code, verify its dependencies, set coding rules and verify if the how code changes are affecting the quality of your application. For this article, we will be analyzing eShopOnWeb (https://github.com/dotnet-architecture/eShopOnWeb), a sample application created by Microsoft to demonstrate architectural patterns on Dotnet Core. It’s an Asp.NET MVC Core 2.2 app that shows a sample shopping app, that can be run on premises or in Azure or in containers, using a Microservices architecture. It has a companion book that describes the application architecture and can be downloaded at https://aka.ms/webappebook.

When you download, compile and run the app, you will see something like this:

You have a full featured Shopping app, with everything you would expect to have in this kind of app. The next step is to start analyzing it.

Download NDepend (you have a 14 day trial to use and evaluate it), install it in your machine, it will install as an Add-In to Visual Studio. The, in Visual Studio, select Extensions/NDepend/Attach new NDepend Project to current VS Solution. A window like this opens:

It has the projects in the solution selected, you can click on Analyze 3 .NET Assemblies. After it runs, it opens a web page with a report of its findings:

This report has an analysis of the project, where you can verify the problems NDepend found, the project dependencies, and drill down in the issues. At the same time, a window like this opens in Visual Studio:

If you want a more dynamic view, you can view the dashboard:

 

In the dashboard, you have a full analysis of your application: lines of code, methods, assemblies and so on. One interesting metric there is the technical debt, where you can see how much technical debt there is in this app and how long will it take to fix this debt (in this case, we have 3.72% of technical debt and 2 days to fix it. We have also the code metrics and violated coding rules. If you click in some item in the dashboard, like the # of Lines, you will see the detail in the properties window:

If we take a look at the dashboard, we’ll see some issues that must be improved. In the Quality Gates, we have two issues that fail. By clicking on the number 2 there, we see this in the Quality Gates evolution window:

If we hover the mouse on one of the failed issues, we get a tooltip that explains the failure:

If we double-click in the failure we drill-down to what caused it:

If we click in the second issue, we see that there are two names used in different classes: Basket and IEmailSender:

Basket is the name of the class in Microsoft.eShopWeb.Web.Pages.Shared.Components.BasketComponent and in Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate

One other thing that you can see is the dependency graph:

With it, you can see how the assemblies relate to each other and give a first look on the architecture. If you filter the graph to show only the application assemblies, you have a good overview of what’s happening:

The largest assembly is Web, followed by Infrastructure and ApplicationCore. The application is well layered, there are no cyclic calls between assemblies (Assembly A calling assembly B that calls A), there is a weak relation between Web and Infrastructure (given by the width of the line that joins them) and a strong one between Web and ApplicationCore. If we have a large solution with many assemblies, just with that diagram, we can take a look of what’s happening and if we are doing the things right. The next step is go to the details and look at the assemblies dependencies. You can hover the assembly and get some info about it:

For example, the Web assembly has 58 issues detected and has an estimated time to fix them in 1 day. This is an estimate calculated by NDepend using the complexity of the methods that must be fixed, but you can set your own formula to calculate the technical debt if this isn’t ok for your team.

Now that we got an overview of the project, we can start fixing the issues. Let’s start with the easiest ones :-).  The Infrastructure assembly has only two issues and a debt of 20 minutes. In the diagram, we can right-click on the Infrastructure assembly and select Select Issues/On Me and on Child Code elements. This will open the issues in the Queries and Rules Edit window, at the right:

We can then open the tree and double click on the second issue. It will point you to a rule “Non-static classes should be instantiated or turned to static”, pointing to the SpecificatorEvaluator<T> class. This is a class that has only one static method and is referenced only one time, so there’s no harm to make it static.  Once you make it static, build the app and run the dependency check again, you will see this:

Oh-Oh. We fixed an issue and introduced another one – an API Breaking Change – when we made that class static, we removed the constructor. In this case, it wasn’t really an API change, because nobody would instantiate a class with only static methods, so we should do a restart, here. Go to the Dashboard, in Choose Baseline and select define:

Then select the most recent analysis and click OK.  That will open the NDepend settings, where you will see the new baseline. Save the settings and rerun the analysis and the error is gone.

We can then open the tree again and double click on another issue that remains in Infrastructure. That will open the source code, pointing to a readonly declaration for a DBContext. This is not a big issue, it’s only telling us that we are declaring the variable as readonly, but the object it’s storing is mutable, so it can be changed. There is a mention of this issue in the Framework Design Guidelines, by Microsoft – https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/. If you hover the mouse on the issue, there is a tooltip on how to fix it:

We have three ways to fix this issue:

  • Remove the readonly from the field
  • Make the field private and not protected
  • Use an attribute to say “ok, I am aware of this, but I don’t mind”

The first option will suppress the error, but will remove what we want to do – show that this field should not be entirely replaced with another dbcontext. The second option will remove the possibility to use the dbcontext in derived classes, so I’ll choose the third option and add the attribute. If I right-click on the issue in the Rules and Queries window and select Suppress Issue, a window opens:

All I have to do is to copy the attribute to the clipboard and paste it into the source code. I also have to declare the symbol CODE_ANALYSIS in the project (Project/Properties/Build). That was easy! Let’s go to the next one.

This is an obsolete method used. Fortunately, the description shows us to use the UseHiLo method. We change the method, run the app to see if there’s nothing broken, and we’re good. W can run the analysis again and see what happened:

We had a slight decrease in the technical debt, we solved one high issue and one violated rule. As you can see, NDepend not only analyzes your code, but it also gives you a position on what you are doing with your code changes. This is a very well architected code (as it should be – it’s an architecture sample), so the issues are minor, but you can see what can be done with NDepend. When you have a messy project, this will be surely an invaluable tool!