Building an ASP.NET Core Web App from Scratch
Published: 2020-11-22 23:41:36

#Get Started 
* https://github.com/OdeToCode/OdeToFood
* Start with an asp.net core web application in visual studio
* Select web application (not web application mvc) with defaults
* Add a asp-page to _Layout pointing to Restaurats/list
* Create a folder for Restaurants
* Add a Razor Page (empty) called List in the Restaurants folder

#Adding a property to List 
(think of this as model in MVC)
* inside the class, type prop TAB TAB string TAB TAB Message that will leave you with `public string Message { get; set; }`
* In appsettings.config add a "Message": "hello world" 
* Back in List type ctor TAB TAB (this will add a constructor)
* Add IConfiguration to () and hit CTRL + . then enter and call the variable config (looks like `public ListModel(IConfiguration config)`
* hit CTRL + . on config and select "create and assign field config" 
* in the OnGet() method add `Message = config["Message"];`

#Options
If you want to be able to refresh your browser while debugging in asp.net core, you need to add this nuget package: 
* https://stackoverflow.com/questions/54106058/why-does-page-not-update-after-refresh-when-cshtml-changes
* need to use the 3.x version if you are using .net core 3.x

If you want to use code generators, powershell in to project directory: 
* dotnet tool install --global dotnet-aspnet-codegenerator 
* dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design -v 3.1
* dotnet aspnet-codegenerator razorpage -h
* dotnet aspnet-codegenerator razorpage List2 Empty -udl -outDir .\Pages\Restaurants

#Add Model
* Right Click Solution and Add Project
* Choose Class Library (.net core)
* Delete the created class and add your own class, call it Restaurant, make it public
* Add a int Id, string Name, and String Location property
* Add an Enum and use CTRL  + . to put it into its own class file
```
public enum CuisineType
        {
            None, 
            Mexican, 
            Chinese
        }
```
* Add a property of type CuisineType
* CTRL+K then CTRL+D to format

#Add Data Access
* Add another library class
* Delete the class and add an interface
* Add an Interface with a GetAll() method
* Implement a public class based on the interface
* Add a readonly list of restaurants
* You need to add a reference to the Restaurant class project to this project, and then add a using statement so that the restaurants is available in the data class
* Create a constructor that populates the list of restaurants
* Implement GetAll method - need to add a reference to System.Linq to get the orderby to work

#Add Singleton of the data access to startup
* Add this to the startup page `services.AddSingleton<IRestaurantData, InMemoryRestaurantData>();`

#Add Data to List
* Add the IRestaurantData interface to the data list constructor
* CTRL + . on IRestaurantData and add "create and assign field config" - should look something like this `public ListModel(IConfiguration config, IRestaurantData restaurantData)`

#Add Search
* Add a form with method=get and no action (will point to itself). 
* Include a input type=search name=searchTerm
* include an i tag with a class="fa-search" 
* To get font awesome to work, you need to get a fontawesome.io url and add it to your URL; if you want autocomplete you need to right click on the project name > add > client side library > provider=unpkg, search for font awesome.  Code is like `<i class="fas fa-search"></i>`
* Replace the GetAll on the restaurants interface with a GetRestaurantByName
* In the implementation method, specify that the name is a string name=null, meaning that if not provided no filtering. 
* Update the linq query to include a `where string.IsNullOrEmpty(name) || r.Name.StartsWith(name)` that includes all if no filter (remember this filter is currently case sensitive)
* Change the OnGet method of List code behind to include `Restaurants = restaurantData.GetRestaurantsByName(searchTerm);` here the searchTerm is pulled from the Request based on the name of the input field

#Show Searched For Term
* Instead of pulling the searchTerm from the request, specify a property with a capital S SearchTerm and a BindProperty that means it will Bind when the request is received
```
[BindProperty(SupportsGet =true)]
public string SearchTerm { get; set; }
```
* Replace name and value on input form to asp-for="SearchTerm" `<input type="search" class="form-control" asp-for="SearchTerm" />`

#Make a Details Page
* Right Click Restaurants folder, Add, Razor Page (empty)
* Add `public Restaurant Restaurant { get; set; }` to the PageModel
* On the cshtml, you can now use `@Model.Restaurant.Name`
* You can make embed the restaurantId in the url with `page "{restaurantId:int}"`

#Make an Edit Page
* Largely the same as Details, but sometimes you need to use @Model and sometimes not

```
<form method="post">
    <input type="hidden" asp-for="@Model.Restaurant.Id" />
    <div class="form-group">
        <label asp-for="@Model.Restaurant.Name"></label>
        <input asp-for="@Model.Restaurant.Name" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="@Model.Restaurant.Location"></label>
        <input asp-for="@Model.Restaurant.Location" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="@Model.Restaurant.Name"></label>
        <select class="form-control" asp-for="Restaurant.Cuisine" asp-items="Model.Cuisines" >
        </select>
    </div>
</form>
```

* In the PageModel, you need to pull in the IHTMLHelper

```
public class EditModel : PageModel
    {
        private readonly IRestaurantData restaurantData;
        private readonly IHtmlHelper htmlHelper;

        public  Restaurant Restaurant { get; set; }
        public IEnumerable<SelectListItem> Cuisines { get; set; }
        public EditModel(IRestaurantData restaurantData, IHtmlHelper htmlHelper)
        {
            this.restaurantData = restaurantData;
            this.htmlHelper = htmlHelper;
        }
        public IActionResult OnGet(int restaurantId)
        {
            Cuisines = htmlHelper.GetEnumSelectList<CuisineType>(); 
            Restaurant = restaurantData.GetById(restaurantId); 
            if (Restaurant == null)
            {
                RedirectToPage("./NotFound");
            }
            return Page(); 
        }
    }
```

#Connect to Local SQL
* Add a connection string to localdb
```
"ConnectionStrings": {
    "OdeToFoodDb":  "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=OdeToFood;Integrated Security=True;"
  }
```
* Wire up the app settings via configure services using an options lambda expression: 
```
            services.AddDbContextPool<OdeToFoodDbContext>(options => {
                options.UseSqlServer(Configuration.GetConnectionString("OdeToFoodDb")); 
            }); 

```
* Add a constructor to the DbContext that pulls in the options from the app settings: 
```
public OdeToFoodDbContext(DbContextOptions<OdeToFoodDbContext> options) : base(options)
        {
        }
```
* Finally need to tell the .Data project to look at the base project for the options information `dotnet ef dbcontext info -s ..\OdeToFood\OdeToFood.csproj`

#Add a Migration
* in the CLI run `dotnet ef migrations add initialcreate -s ..\OdeToFood\OdeToFood.csproj`
* then apply the migration `dotnet ef database update -s ..\OdeToFood\OdeToFood.csproj`

#Implement Delete Functionality
* Add a Delete function `Restaurant Delete(int id); `
* CTRL+. on the InMemoryRestaurantData and select option to move to its own class file
* in the new file, CTRL+. and select to implement interface (this will add the delete) 
* Add the delete code
```
       {
            var restaurant = restaurants.FirstOrDefault(r => r.Id == id); 
            if (restaurant != null)
            {
                restaurants.Remove(restaurant); 
            }
            return restaurant; 
        }
```

#Add the SQL
* on the IRestaurantData file, type this: 
```
    public class SqlRestaurantData : IRestaurantData
    {

    }
```
* CTRL+. and SqlRestaurantData and select option to move to its own file
* in the new file, CTRL+. implement the interface