Friday, December 29, 2023

Using MOQ

MOQ is a popular mocking framework for .NET used to create mock objects for testing. MOQ allows you to isolate the class under test by replacing its dependencies with controlled mock objects, making it easier to test behavior in isolation.  Here are the steps to use it.  

1.  Install MOQ.  Add MOQ using the NuGet package manager.  https://www.nuget.org/packages/Moq

2.  Create a Mock Object. Use Mock<T> where T is the type you want to mock. For example:

_webAPIClientMock = new Mock<IWebAPIClient>();

3,  Setup Method Behavior. Define how the mock object should behave. For example:

_webAPIClientMock.Setup(client => client.GetApplicationSetting(It.IsAny<GetAppSettingRequest>()).Result).Returns(appSettingResponse);

4.  Inject the mock. Inject the mock object into the class you are testing.

_apiSettingsProvider = new ApiSettingsProvider(Mock.Of<ILogger<ApiSettingsProvider>>(), _webAPIClientMock.Object);

5.  Verify Interactions. Optionally, verify that certain interactions with the mock object occurred, like _webAPIClientMock.Verify(x => x.GetApplicationSetting(), Times.Once);

6.  Run Your Test.  Execute your test method as usual.

Full Example

[TestClass]
public class ApiSettingsProviderTests
{
private  Mock<IWebAPIClient> _webAPIClientMock;
private  ApiSettingsProvider _apiSettingsProvider;

[TestInitialize]
public void Initialize()
{
_webAPIClientMock = new Mock<IWebAPIClient>();
_apiSettingsProvider = new ApiSettingsProvider(Mock.Of<ILogger<ApiSettingsProvider>>(), _webAPIClientMock.Object);
}

[TestMethod]
public void GetAppSettingValue_ShouldReturnFirstValueFromAppSettingsResponse()
{
   // Arrange
var appSettingName = "TestSetting";
var settingValues = new string[] { "Value1", "Value2" };
var appSettingResponse = new GetAppSettingResponse
{
SettingValues = new List<string>(settingValues)
};
_webAPIClientMock.Setup(client => client.GetApplicationSetting(It.IsAny<GetAppSettingRequest>()).Result).Returns(appSettingResponse);
// Act
var value = _apiSettingsProvider.GetAppSettingValue(appSettingName);
// Assert
Assert.AreEqual("Value1", value);
}
}

Resources

Creating Angular Tests and Mocking with Jasmine

When creating Angular Components, by default a spec.ts file is created.  By definition, a unit test should not call any files, databases, or services.  We use a mock, or a stand in for services.  

Mocking Services

To mock services, the jasmine.createSpyObj is used. Example:

loggingService = jasmine.createSpyObj('LoggingService', ['Info','Error']); 

Providing Mocked Services

To use the mocked service, it is provied.  Example:

    TestBed.configureTestingModule({
      providers: [
        ErrorHandlerService, 
        { provide: LoggingService, useValue: loggingService },
        { provide: HttpTestingController, useValue: httpTestingController }
      ],
    });


Full Example

We have an error handler service and we want to ensure that the message is logged by using the Error method.

import { TestBed } from '@angular/core/testing';
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHandlerService } from './error-handler.service';
import { LoggingService } from './logging.service';
import { HttpTestingController } from '@angular/common/http/testing';

describe('ErrorHandlerService', () => {
  let errorHandlerService: ErrorHandlerService;
  let loggingService: LoggingService;
  let httpTestingController: HttpTestingController;
  
  beforeEach(() => {
    loggingService = jasmine.createSpyObj('LoggingService', ['Info','Error']);
    TestBed.configureTestingModule({
      providers: [
        ErrorHandlerService, 
        { provide: LoggingService, useValue: loggingService },
        { provide: HttpTestingController, useValue: httpTestingController }
      ],
    });
    errorHandlerService = TestBed.inject(ErrorHandlerService);
    httpTestingController = TestBed.inject(HttpTestingController);
  });
  it('should be created', () => {
    expect(errorHandlerService).toBeTruthy();
  });
  it('should handle an error and log it', () => {
    spyOn(console, 'error');
    const error = new Error('Test error message');
    errorHandlerService.handleError(error);
    expect(console.error).toHaveBeenCalledWith(error);
    expect(loggingService.Error).toHaveBeenCalledWith(error.message, [error]);
  });
  
});



Thursday, October 5, 2023

Angular with .NET Core Powershell Setup Script

Here is a Powershell script that I made to automatically install everything that is needed to develop with Angular 16 and .NET Core 7.

Here's a breakdown:


1. Chocolatey Installation Check: At the beginning, the script checks if Chocolatey, a package manager for Windows, is already installed. If it is found at the specified location (`C:\ProgramData\chocolatey`), it prints a message indicating that Chocolatey is installed. If it is not found, the script adjusts the execution policy to allow the script to run, then it fetches and executes the installation script for Chocolatey from its official website.

2.  API Software Installation: Through the Chocolatey package manager, the script proceeds to install the .NET 7.0 SDK, Visual Studio 2022 Community edition, web and data workloads for Visual Studio 2022, SQL Server Management Studio, and Postman. The flags `-y`, `--allow-empty-checksums`, and `--ignore-checksum` are used to automatically approve installations and bypass checksum verifications, which are useful in automated environments but can present security risks if the sources are not trusted.

3. UI Software Installation: For front-end or user interface development, the script installs Node.js LTS version and Visual Studio Code (VSCode) using Chocolatey.

4. VS Code Extensions Installation: Once VSCode is installed, the script installs a series of extensions for it. These extensions range from linters like ESLint, support for Angular development, CSS formatting tools, IntelliSense for paths and classes, and other utility extensions like auto-rename tags, icons, Prettier formatter, and GitHub's Copilot and Copilot Chat.

5. Angular CLI Installation: Finally, the script installs the Angular Command-Line Interface (CLI) globally using npm, the Node.js package manager. The Angular CLI is a vital tool for Angular developers, allowing them to create, manage, and deploy Angular applications with ease.

In summary, this script serves as a one-stop solution to set up a development environment with all the necessary tools and extensions for both API and UI development, primarily targeting .NET and Angular technologies.


# Install Chocolatey if it does not exist
if (Test-Path 'C:\ProgramData\chocolatey') {
    Write-Host "Chocolatey is installed."
} else {
    Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
}

# API Software
& choco install dotnet-7.0-sdk -y --allow-empty-checksums --ignore-checksum
& choco install visualstudio2022community -y --allow-empty-checksums --ignore-checksum
& choco install visualstudio2022-workload-netweb -y --allow-empty-checksums --ignore-checksum
& choco install visualstudio2022-workload-data -y --allow-empty-checksums --ignore-checksum
& choco install sql-server-management-studio -y --allow-empty-checksums --ignore-checksum
& choco install postman -y --allow-empty-checksums --ignore-checksum

# UI Software
& choco install nodejs-lts -y --allow-empty-checksums --ignore-checksum
& choco install vscode -y --allow-empty-checksums --ignore-checksum

# VS Code Extensions
& "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
& "C:\Program Files\Microsoft VS Code\bin\code" --install-extension formulahendry.auto-rename-tag
& "C:\Program Files\Microsoft VS Code\bin\code" --install-extension vscode-icons-team.vscode-icons
& "C:\Program Files\Microsoft VS Code\bin\code" --install-extension esbenp.prettier-vscode
& "C:\Program Files\Microsoft VS Code\bin\code" --install-extension GitHub.copilot
& "C:\Program Files\Microsoft VS Code\bin\code" --install-extension GitHub.copilot-chat

# Install Angular CLI
& npm install -g @angular/cli

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:



Wednesday, January 25, 2023

Rapidly Setting Up A Development PC with Chocolatey

This is an update to my earlier post:  https://sixfootcoder.blogspot.com/2016/04/rapidly-setting-up-azure-pc-with.html

To rapidly setup a development PC.


Install Chocolately

https://chocolatey.org/install


Create InstallApp.bat

Create a batch file that will install individual applications called InstallApp.bat

echo *******************************************************************************
echo %1 is installing now !
echo *******************************************************************************
cinst %1 -y --allow-empty-checksums --ignore-checksum


Create DesktopInstall.bat

Create a batch file called DesktopInstall.bat that will install individual applications with InstallApp.bat.  Add REM before any entry you don't want.

REM Media
call InstallApp paint.net
call InstallApp picpick.portable

REM Web
call InstallApp firefox
call InstallApp googlechrome

REM Video Conferencing and Messaging
call InstallApp microsoft-teams.install
call InstallApp slack

REM Utilities
call InstallApp 7zip
call InstallApp filezilla
call InstallApp chocolateygui
call InstallApp winmerge
call InstallApp putty.install

REM Developer Tools before Visual Studio
call InstallApp expresso
call InstallApp git.install
call InstallApp tortoisegit
call InstallApp markdownmonster
call InstallApp postman
call InstallApp nugetpackageexplorer

REM VS Code
call InstallApp vscode.install
call InstallApp vscode-prettier
call InstallApp vscode-eslint

REM Node
call InstallApp nodejs.install

REM Visual Studio 2019
call InstallApp visualstudio2019community
call InstallApp visualstudio2019-workload-netweb

REM Visual Studio 2022
call InstallApp visualstudio2022community
call InstallApp visualstudio2022-workload-netweb