Saturday, September 30, 2023

Software Development Requirements for Angular

Install the LTS Version of Node

https://nodejs.org

Or install with Chocolatey

choco install nodejs-lts


Install the Angular CLI

At the command prompt

npm install -g @angular/cli


Install VS Code

https://code.visualstudio.com/

Or install with Chocolatey

choco install vscode


Install VS Code Extensions

ESLint: https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint

Angular Snippets (Version 16): https://marketplace.visualstudio.com/items?itemName=johnpapa.Angular2

CSS Formatter: https://marketplace.visualstudio.com/items?itemName=aeschli.vscode-css-formatter

Angular Language Service: https://marketplace.visualstudio.com/items?itemName=Angular.ng-template

IntelliSense for CSS class names in HTML: https://marketplace.visualstudio.com/items?itemName=Zignd.html-css-class-completion

Path Intellisense: https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense

Or install on the command line:


"C:\Program Files\Microsoft VS Code\bin\code" --install-extension dbaeumer.vscode-eslint

"C:\Program Files\Microsoft VS Code\bin\code" --install-extension johnpapa.Angular2

"C:\Program Files\Microsoft VS Code\bin\code" --install-extension aeschli.vscode-css-formatter

"C:\Program Files\Microsoft VS Code\bin\code" --install-extension Angular.ng-template

"C:\Program Files\Microsoft VS Code\bin\code" --install-extension Zignd.html-css-class-completion

"C:\Program Files\Microsoft VS Code\bin\code" --install-extension christian-kohler.path-intellisense

Friday, September 29, 2023

Developer Software Requirements for Creating a .NET Core Web API

Install .NET 7 SDK

https://dotnet.microsoft.com/en-us/download

Or Install with Chocolatey

choco install dotnet-7.0-sdk

Install Visual Studio 2022

https://visualstudio.microsoft.com

Choose workloads

ASP.NET and Web Development

Data Storage and Processing

Or Install  One of These with Chocolatey

choco install visualstudio2022community

choco install visualstudio2022professional

choco install visualstudio2022enterprise

Then install workloads

choco install visualstudio2022-workload-netweb

choco install visualstudio2022-workload-data


Install SQL Server Developer Edition

https://www.microsoft.com/en-us/sql-server/sql-server-downloads

Choose Basic


Install SQL Server Management Studio or Database .NET

SQL Server Management Studio

https://learn.microsoft.com/en-us/sql/ssms/download-sql-server-management-studio-ssms?view=sql-server-ver16

Or install with Chocolatey

choco install sql-server-management-studio


Database.NET

https://fishcodelib.com/database.htm

Or install with Chocolatey

choco install databasenet

Install Postman 

https://www.postman.com/downloads/

Or install with Chocolatey

choco install postman

Thursday, September 28, 2023

Angular 16 CLI Resources

 

Links

Usage

Installation
Install LTS version of Node: https:­//n­ode­js.org or install with Chocol­atey: choco install nodejs-lts
npm install -g @angular/cli

New Project
ng new <project-name>
Generate a component
ng generate component <component-name>
Generate a service
ng generate service <service-name>

Run the server on http://localhost:4200
ng serve --open

Videos

Tutorials

Cheat Sheets

Wednesday, September 27, 2023

Troubleshooting Deploying Blazor to a Linux App Service in Azure

It all started one day when I decided to try and deploy the Bed Brigade National Website (which is a Blazor Server application) to Azure using Github actions.  

The github repo is located here:  https://github.com/GregFinzer/BedBrigadeNational

Thank God for Patrick God and Tim Corey who have excellent tutorials on YouTube.  Here are the videos that I followed when deploying to Azure:

Create Web App in Azure: https://www.youtube.com/watch?v=NiBEk8QepP4

Deploy Blazor with Github actions:  https://www.youtube.com/watch?v=wybpWMrpuZk

Updating the Connection String in Blazor:  https://www.youtube.com/watch?v=lSCjtrNkT38

Deployment is always difficult and deploying to Azure has its share of pain.  Only the first exception was obvious.    

Could not find part of the path

I had volunteers working on the website and they were using non-standard path combination.  In other words, they were making a windows platform specific path like this:

string directory = appRoot + "\\" + folder + "\\" + filename;

The platform agnostic way to do this so it works under Linux:

string directory = Path.Combine(appRoot, folder, filename)


SQL Server Transient Failure

If you are using Basic B1 and a low tier for SQL Server, get ready for a lot of retries.  This is even if you have caching enabled in your application.  Here is my previous post on caching:  https://sixfootcoder.blogspot.com/2018/09/using-caching-in-net-framework.html


You will get this error message:

An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call.  

In order to fix this, you have to add a line to your context factory with EnableRetryOnFailure.
  
builder.Services.AddDbContextFactory<DataContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), sqlBuilder =>
    {
        sqlBuilder.EnableRetryOnFailure(5, TimeSpan.FromSeconds(10), null);
    });
    options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
    options.UseApplicationServiceProvider(_svcProvider);
});


Unable to resolve service for type IResponseCompressionProvider

Blazor Server does not support Response Compression.  You might be seeing this exception:

Unable to resolve service for type 'Microsoft.AspNetCore.ResponseCompression.IResponseCompressionProvider' while attempting to activate 

If you have this line in your program.cs, remove it as that is causing the error:

app.UseResponseCompression();

Could not load assembly DependencyModel

There is a known issue with Serilog and Azure.  You may be seeing this exception:

Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly 'Microsoft.Extensions.DependencyModel

There are two fixes that you have to make for this:

Add a reference to these NuGet packages in your client project:
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.DependencyModel

If you have a tests project, go to the properties of the project.  Under Application and then Packaging, uncheck:  Allow publishing outside Visual Studio, such as "dotnet publish"

Connection Timeout Expired

You might be seeing this error:  

Microsoft.Data.SqlClient.SqlException (0x80131904): Connection Timeout Expired.  The timeout period elapsed during the post-login phase.  The connection could have timed out while waiting for server to complete the login process and respond; Or it could have timed out while attempting to create multiple active connections.  The duration spent while attempting to connect to this server was - [Pre-Login] initialization=162; handshake=279; [Login] initialization=0; authentication=1; [Post-Login] complete=29565; 

This was the worst error to deal with.  I can't believe that this is a default Azure option for SQL Server but it is.  SQL Server has an Auto Pause feature that if it is not used then it shuts down.  Supposedly it should come back up when accessed again but it does not.  There is a defect.  It shuts down and stays down.  

To correct this issue, click on the SQL Service then SQL Databases, then the name of your database and finally Compute + Storage.  Uncheck Enable auto-pause.




The other option you should do is to enable Always On for your App Service.  Click on your App Service, then Configuration, then General settings, and then Always On.



Best Practices when using Swagger with .NET Core

 Take your Swagger documentation to the next level by using several different configuration options:

  1. Replace your existing AddSwaggerGen and add information about your project.
  2. Pull your existing XML Documentation into Swagger
  3. Use Swagger Annotations to define return types and status codes.

Alter AddSwaggerGen

In your program.cs of your project, replace your AddSwagerGen with the following code, altering to the needs of your project.

var apiInfo = new OpenApiInfo
{
    Title = "Blogs API",
    Version = "v1",
    Description = "An API to perform Blog operations",
    TermsOfService = new Uri("https://example.com/terms"),
    Contact = new OpenApiContact
    {
        Name = "John Whorfin",
        Email = "John.Whorfin@gmail.com",
        Url = new Uri("https://twitter.com/jwhorfin"),
    },
    License = new OpenApiLicense
    {
        Name = "Blog API LICX",
        Url = new Uri("https://example.com/license"),
    }
};

var securityScheme = new OpenApiSecurityScheme
{
    Name = "Authorization",
    Type = SecuritySchemeType.Http,
    Scheme = "bearer",
    BearerFormat = "JWT",
    In = ParameterLocation.Header,
    Description = "JWT Authorization header using the Bearer scheme."
};

var securityRequirement = new OpenApiSecurityRequirement
{
    {
        new OpenApiSecurityScheme
        {
            Reference = new OpenApiReference
            {
                Type = ReferenceType.SecurityScheme,
                Id = "Bearer"
            }
        },
        Array.Empty<string>()
    }
};


// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", apiInfo);
    options.AddSecurityDefinition("Bearer", securityScheme);
    options.AddSecurityRequirement(securityRequirement);
    
    // using System.Reflection;
    var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
});


Generate XML Documentation

Open the properties file of your project.  Scroll down to the Build and then Output.  



Use Swagger Annotations on a Controller

Make sure you have a using Swashbuckle.AspNetCore.Annotations in your controller



using Swashbuckle.AspNetCore.Annotations;

[Route("api/[controller]")]
[ApiController]
public class CategoriesController : ControllerBase
{
    private readonly ICategoryRepository _categoryRepository;

    public CategoriesController(ICategoryRepository categoryRepository)
    {
        _categoryRepository = categoryRepository;
    }
    
    /// <summary>
    /// Get category by Id
    /// </summary>
    /// <remarks>Returns a single category</remarks>
    /// <param name="categoryId">Id of category to return</param>
    /// <param name="cancellationToken"></param>
    /// <response code="200">successful operation</response>
    /// <response code="401">Not authorized</response>
    /// <response code="404">Category not found</response>
    [HttpGet]
    [Consumes("application/json")]
    [Route("{categoryId:Guid}")]
    [SwaggerOperation("GetCategoryById")]
    [SwaggerResponse(statusCode: 200, type: typeof(Category), description: "Successful operation")]
    [SwaggerResponse(statusCode: 404, type: typeof(ApiError), description: "Category not found")]
    [ProducesResponseType(typeof(GetCategoryResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetCategoryById([FromRoute] Guid categoryId, CancellationToken cancellationToken)
    {
        //Implementation
    }

    /// <summary>
    /// Get all categories
    /// </summary>
    /// <param name="cancellationToken"></param>
    /// <remarks>Returns all categories</remarks>
    /// <response code="200">Successful Operation</response>
    /// <response code="401">Not authorized</response>
    [HttpGet]
    [Consumes("application/json")]
    [Route("/Categories/GetAllCategories")]
    [SwaggerOperation("GetAllCategories")]
    [SwaggerResponse(statusCode: 200, type: typeof(Category), description: "Successful Operation")]
    [ProducesResponseType(typeof(List<Category>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetAllCategories(CancellationToken cancellationToken)
    {
        //Implementation
    }

    /// <summary>
    /// Add a new category
    /// </summary>
    /// <remarks>Adds a new category</remarks>
    /// <param name="body">Category object that needs to be added</param>
    /// <param name="cancellationToken"></param>
    /// <response code="201">Category created</response>
    /// <response code="400">Invalid input</response>
    /// <response code="401">Not authorized</response>
    [HttpPost]
    [Consumes("application/json")]
    [SwaggerOperation("AddCategory")]
    [SwaggerResponse(statusCode: 201, type: typeof(CategoryRequest), description: "Category created")]
    [SwaggerResponse(statusCode: 400, type: typeof(ApiError), description: "Invalid input")]
    [ProducesResponseType(typeof(Category), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> AddCategory([FromBody] CategoryRequest body, CancellationToken cancellationToken)
    {
        //Implementation
    }

    /// <summary>
    /// Update category
    /// </summary>
    /// <remarks>Updates category</remarks>
    /// <param name="body">Category object that needs to be updated</param>
    /// <param name="categoryId"></param>
    /// <param name="cancellationToken"></param>
    /// <response code="200">successful operation</response>
    /// <response code="401">Not authorized</response>
    /// <response code="404">Category not found</response>
    [HttpPut]
    [Route("{categoryId:guid}")]
    [Consumes("application/json")]
    [SwaggerOperation("UpdateCategory")]
    [SwaggerResponse(statusCode: 200, type: typeof(Category), description: "Successful Operation")]
    [SwaggerResponse(statusCode: 404, type: typeof(ApiError), description: "Category not found")]
    [ProducesResponseType(typeof(Category), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> UpdateCategory([FromBody] CategoryRequest body, [FromRoute] Guid categoryId, CancellationToken cancellationToken)
    {
        //Implementation
    }    


    /// <summary>
    /// Deletes a category by Id
    /// </summary>
    /// <param name="categoryId">Category id to delete</param>
    /// <param name="cancellationToken"></param>
    /// <response code="200">successful operation</response>
    /// <response code="401">Not authorized</response>
    /// <response code="404">Category not found</response>
    [HttpDelete]
    [Consumes("application/json")]
    [Route("{categoryId:Guid}")]
    [SwaggerOperation("DeleteCategory")]
    [SwaggerResponse(statusCode: 200, type: typeof(Category), description: "Successful Operation")]
    [SwaggerResponse(statusCode: 404, type: typeof(ApiError), description: "Category not found")]
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> DeleteCategory(Guid categoryId, CancellationToken cancellationToken)
    {
        //Implementation
    }
}

References: