Launch ASP.NET Core API from windows service - asp.net

I have a small API created from a ASP.NET core Api template in visual studio 22 that targets .NET6.
For testing\debuging I'm launching the API using console application and everything works.
But for production I need this API to be started from a windows service and I have no idea how to make it.
I could simply put the .exe in same folder as the service and call it, but far as I now if no user logged in the console app won't start.
So the idea is to pack the API together with all services and start it when windows service starts.
I've made a small test by creating a static class name "Test.cs" with a method named "Start" and copy all the code from "Program.cs" and call the "Start" method from a test application.
The API starts and stays listening to the endpoints but for some reason doesn't map the controllers from project controllers folder.
all OK when starting API from default
controllers not mapped when start API from static method
program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
//to avoid json serialize camel casing
builder.Services.AddControllers().AddJsonOptions(opts =>
{
opts.JsonSerializerOptions.PropertyNamingPolicy = null;
opts.JsonSerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals; //para aceitar NAN, infinitos etc no json
});
//////////
//Nedeed
builder.Services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseCors("CorsPolicy");
//app.UseAuthorization();
app.MapControllers();
app.Run();
test.cs
namespace ProjectX.Api
{
public static class Test
{
public static void Start()
{
var builder = WebApplication.CreateBuilder();
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
//to avoid json serialize camel casing
builder.Services.AddControllers().AddJsonOptions(opts =>
{
opts.JsonSerializerOptions.PropertyNamingPolicy = null;
opts.JsonSerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals; //para aceitar NAN, infinitos etc no json
});
//////////
//Nedeed
builder.Services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.UseCors("CorsPolicy");
//app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
tester app
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace ProjectX.Tester
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
try
{
ProjectX.Api.Test.Start();
}
catch (Exception ex )
{
}
}
}
}

You can run the exe file of Web Api directly in Start():
public static void Start()
{
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = false;
startInfo.UseShellExecute = false;
startInfo.FileName = "Your Path\\ProjectName.exe";
startInfo.WindowStyle = ProcessWindowStyle.Normal;
startInfo.Arguments = "";
try
{
using (Process exeProcess = Process.Start(startInfo))
{
exeProcess.WaitForExit();
}
}
catch
{
//Log error.
}
}
Then you can successfully call the API in the project:
If you want to apply Swagger, please add the following code in the production environment in Program.cs:
app.UseSwagger();
app.UseSwaggerUI();
Result:
Hope this can help you.

Related

How to do a health check on a POST url in ASP.NET/blazor

I am trying to implement health checks in my blazor application. To do so, I have used the Microsoft.AspNetCore.Diagnostics.HealthChecks package among others. Below you can see sql and url health checks.
startup.cs
//using AjuaBlazorServerApp.Data;
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace AjuaBlazorServerApp
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddHostedService<PeriodicExecutor>();
services.AddHealthChecks().AddUrlGroup(new Uri("https://api.example.com/post"),
name: "Example Endpoint",
failureStatus: HealthStatus.Degraded)
.AddSqlServer(Configuration["sqlString"],
healthQuery: "select 1",
failureStatus: HealthStatus.Degraded,
name: "SQL Server");
services.AddHealthChecksUI(opt =>
{
opt.SetEvaluationTimeInSeconds(5); //time in seconds between check
opt.MaximumHistoryEntriesPerEndpoint(60); //maximum history of checks
opt.SetApiMaxActiveRequests(1); //api requests concurrency
opt.AddHealthCheckEndpoint("Ajua API", "/api/health"); //map health check api
}).AddInMemoryStorage();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
endpoints.MapHealthChecks("/api/health", new HealthCheckOptions()
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.
WriteHealthCheckUIResponse
});
endpoints.MapHealthChecksUI();
});
}
}
}
The sql one works perfectly. However the url health check returns the following error:
Discover endpoint #0 is not responding with code in 200...299 range, the current status is MethodNotAllowed.
What i would like to know is if there is a way to maybe set the method type and if need be send some test details to the endpoint so that we can actually get a valid response.
AddUrlGroup has an overload that allows you to specify the method through the httpMethod parameter. Try using :
.AddUrlGroup(new Uri("https://api.example.com/post"),
httpMethod: HttpMethod.Post,
name: "Example Endpoint",
failureStatus: HealthStatus.Degraded)
Another overload allows configuring the HttpClient and HttpMessageHandler explicitly, to add specific default headers for example, enable compression or redirection.
.AddUrlGroup(new Uri("https://api.example.com/post"),
httpMethod: HttpMethod.Post,
name: "Example Endpoint",
configureClient: client => {
client.DefaultRequest.Headers.IfModifiedSince=
DateTimeOffset.Now.AddMinutes(-10);
},
failureStatus: HealthStatus.Degraded)
Yet another overload allows explicitly configuring the UriHealthCheckOptions class generated by other AddUrlGroup overloads:
.AddUrlGroup(uriOptions=>{
uriOptions
.UsePost()
.AddUri(someUrl,setup=>{
setup.AddCustomHeader("...","...");
});
});
There's no way to specify content headers because the health check code doesn't send a body.
I accepted the answer above because it technically answered my question. However, this is the implementation I ended up using. Basically you will have to create your own custom healthcheck.
Add a new folder under you projects main directory and name it accordingly
2. Create a new class in that folder and add code similar to what I have below
EndpointHealth.cs
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
using System.IO;
using Microsoft.Extensions.Configuration;
namespace BlazorServerApp.HealthChecks
{
public class EndpointHealth : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken =
default)
{
//create a json string of parameters and send it to the endpoint
var data = new
{
test = "Example",
};
string jsonString = JsonSerializer.Serialize(data);
var httpWebRequest = (HttpWebRequest)WebRequest.Create("https://api.example.com/post");
httpWebRequest.ContentType = Configuration["application/json"];
httpWebRequest.Method = Configuration["POST"];
using (var streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()))
{
streamWriter.Write(jsonString);
}
//Get the endpoint result and use it to return the appropriate health check result
var httpResponse = (HttpWebResponse)httpWebRequest.GetResponse();
if (((int)httpResponse.StatusCode) >= 200 && ((int)httpResponse.StatusCode) < 300)
return Task.FromResult(HealthCheckResult.Healthy());
else
return Task.FromResult(HealthCheckResult.Unhealthy());
}
}
}
Then add the following code to the top of your startup.cs file using BlazorServerApp.HealthChecks;
and finally the below code:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddHealthChecks()
.AddCheck<EndpointHealth>("Endpoint",null);
}

Angular + Web API Error Only In Production

About the App
I have an Angular 8 App that uses .Net REST APIs that I inherited from a previous employee (I am new to both frameworks). It has been under development for a few months and has been successfully published to the Production server for testing several times throughout development.
The Issue
After the last publish to the Production server, I am receiving two errors in the console stating Unexpected token < in JSON at position 0 for the API call api/MtUsers/GetLoggedInUser which is called on the backend of the home component. I did not update any code in the home component or the MTUsersController since the last time changes were published to production.
Observations
Error only appears in production
Error still exists if I checkout an older (previously working) commit and publish
Visual Studio started complaining about experimental decorators and missing modules on publish (fixed by restarting VS)
Calling the API using postman appears to return index.html in production but returns the MtUser object in localhost
What I've Tried
Clean solution and re-publish
Checkout last known working commit and publish
Recycle application pool and restart website in IIS
Try various code changes related to website configuration
Relevant Code
I'm not too sure what is most "relevant" to this issue, so I am providing the code specified in the error and the startup.cs file. Let me know if something else would be more useful.
home.component.ts
import { Component } from '#angular/core';
import { MtUser } from 'src/app/core/models/mtUser.model'
import { MtUserService } from 'src/app/core/services/mtUser.service';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
})
export class HomeComponent {
loadingLoggedInUserInfo = true;
loggedInUser: MtUser = <MtUser>{};
/** home ctor */
constructor(
private mtUserService: MtUserService){
document.getElementsByClassName('main-content')[0].scroll(0, 0);
this.mtUserService.GetLoggedInUser()
.subscribe(response => {
this.loadingLoggedInUserInfo = false;
this.loggedInUser = response;
});
}
}
MtUserService
import { Injectable } from '#angular/core';
import { MtUser } from 'src/app/core/models/mtUser.model';
import { environment } from 'src/environments/environment';
import { HttpClient } from '#angular/common/http';
#Injectable({ providedIn: 'root', })
export class MtUserService {
constructor(private http: HttpClient) { }
GetLoggedInUser() {
return this.http.get<MtUser>(environment.apiUrl + '/MtUsers/GetLoggedInUser');
}
}
MtUsersController
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MT8.Data;
using MT8.Models;
using MT8.Utilities;
namespace MT8.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class MtUsersController : Mt8ControllerBase
{
private readonly Mt8Context _context;
public MtUsersController(Mt8Context context)
{
_context = context;
}
// GET: api/MtUsers/GetLoggedInUser
[HttpGet("GetLoggedInUser")]
public async Task<ActionResult<MtUser>> GetLoggedInUser()
{
var loggedInUserName = ApplicationEnvironment.GetLoggedInUserName(this.HttpContext);
var loggedInUser = await this._context.MtUsers
.SingleOrDefaultAsync(u => u.UserName == loggedInUserName);
if (loggedInUser == null)
return NotFound();
return loggedInUser;
}
}
}
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using MT8.Data;
using Microsoft.AspNetCore.Server.IISIntegration;
using System.Threading.Tasks;
using MT8.Utilities;
namespace MT8
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
services.AddCors(options =>
{
options.AddPolicy(MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
services.AddDbContext<Mt8Context>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Mt8Context")));
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddControllers()
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new StringToIntJsonConverter()))
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new StringToNullableIntConverter()))
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new StringToDecimalJsonConverter()))
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new StringToDoubleJsonConverter()))
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new StringToDateTimeJsonConverter()))
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new StringToNullableDateTimeConverter()));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, Mt8Context dbContext)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
if (!env.IsProduction())
dbContext.InitializeData();
}
}
}
After lots of trial and error, I determined the issue was with a connection string in the appsettings.Production.json file. It was originally set to Integrated Security=True
when the application was first built and something caused this to stop working all of a sudden. I updated the database to use an SQL login and provided the ID and password in the connection string which fixed the issue.

Can't call client method from server

I'm trying to use SignalR to broadcast a message from the server to the client without the client triggering the message. From tutorials that I've seen, defining a method in the client, like so:
signalRConnection.client.addNewMessage = function(message) {
console.log(message);
};
should allow the following hub code to be used on the server:
public async Task SendMessage(string message)
{
await Clients.All.addNewMessage("Hey from the server!");
}
However, the Clients.All.addNewMessage call causes an error in the C# compiler:
'IClientProxy' does not contain a definition for 'addNewMessage' and no accessible extension method 'addNewMessage' accepting a first argument of type 'IClientProxy' could be found (are you missing a using directive or an assembly reference?)
How do I fix this? The server code is contained within the hub.
This is because you are using ASP.NET Core SignalR but you are calling client method following ASP.NET MVC SignalR. In ASP.NET Core SignalR you have to call the client method as follows:
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("AddNewMessage", message); // here `AddNewMessage` is the method name in the client side.
}
It showing your client side code is also for ASP.NET MVC SignalR. For ASP.NET Core SignalR it should be as follows:
"use strict";
var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();
connection.on("AddNewMessage", function (message) {
// do whatever you want to do with `message`
});
connection.start().catch(function (err) {
return console.error(err.toString());
});
And In the Startup class SignalR setup should be as follows:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSignalR(); // Must add this
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chatHub"); // Here is configuring for `ChatHub`
});
app.UseMvc();
}
}
Please follow Get started with ASP.NET Core SignalR this tutorial if you face further problem.

Application Insights in IHostedService console application

I am trying to enable Application Insights in a console application using IHostedService (for the moment, it's a simple console application which we run as WebJob, in future in containers).
As far as my knowledge goes, in the following code, so far we do not have any extension to register globally Application Insights as an implementation of ILogger:
public static class Program
{
public static Task Main(string[] args)
{
var hostBuilder = new HostBuilder()
.ConfigureHostConfiguration(config =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddJsonFile("appsettings.json", optional: false);
config.AddEnvironmentVariables();
})
.ConfigureLogging((context, logging) =>
{
logging.AddConfiguration(context.Configuration.GetSection("Logging"));
if (context.HostingEnvironment.IsDevelopment())
{
logging.AddConsole();
}
else
{
//TODO: register ApplicationInsights
}
});
return hostBuilder.RunConsoleAsync();
}
}
So far, I found out that potentially, I should be able to set everything up using custom implementation of the logger, i.e. public class ApplicationInsightsLogger : ILogger, and then... register it in the container so that DI resolves it.
Is this the right direction?
I made an extension that I could use from either an IHost or an IWebHost:
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.ApplicationInsights;
public static class LoggingBuilderExtensions
{
public static ILoggingBuilder AddLogging(this ILoggingBuilder loggingBuilder)
{
loggingBuilder.AddFilter<ApplicationInsightsLoggerProvider>("", LogLevel.Trace);
loggingBuilder.AddAzureWebAppDiagnostics();
loggingBuilder.AddApplicationInsights();
return loggingBuilder;
}
}
Since I'm not sending in the context (HostBuilderContext or WebHostBuilderContext), I can use it in either app type like this:
new HostBuilder().ConfigureLogging(loggingBuilder => loggingBuilder.AddLogging())
or
WebHost.CreateDefaultBuilder().ConfigureLogging(loggingBuilder => loggingBuilder.AddLogging())
If you needed a specific property from the context (like environment type), you could extract that and send it in as a parameter to the extension.
Here's a reference: https://github.com/Microsoft/ApplicationInsights-dotnet-logging/blob/develop/src/ILogger/Readme.md

SignalR as WCF web socket service

Is it possible to host SignalR as a part of WCF websocket service and not as a part of ASP.net web site. I am aware about pushing mesage from a web service to signalR clients but is it also possible tht when the socket connection is opened from browser it maps to a web serivce contract?
You can self-host the SignalR server:
Taken from (https://github.com/SignalR/SignalR/wiki/QuickStart-Hubs):
Getting Started
To get started, Install the following packages:
Install-Package Microsoft.Owin.Hosting -pre
Install-Package Microsoft.Owin.Host.HttpListener -pre
Install-Package Microsoft.AspNet.SignalR.Owin -pre
using System;
using Microsoft.AspNet.SignalR;
using Microsoft.Owin.Hosting;
using Owin;
namespace SignalR.Hosting.Self.Samples
{
class Program
{
static void Main(string[] args)
{
string url = "http://172.0.0.01:8080";
using (WebApplication.Start<Startup>(url))
{
Console.WriteLine("Server running on {0}", url);
Console.ReadLine();
}
}
}
class Startup
{
public void Configuration(IAppBuilder app)
{
// This will map out to http://localhost:8080/signalr by default
// This means a difference in the client connection.
app.MapHubs();
}
}
public class MyHub : Hub
{
public void Send(string message)
{
Clients.All.addMessage(message);
}
}
}
You can host the SignarR hub in any .Net application, like:
public class Program
{
public static void Main(string[] args)
{
// Connect to the service
var hubConnection = new HubConnection("http://localhost/mysite");
// Create a proxy to the chat service
var chat = hubConnection.CreateProxy("chat");
// Print the message when it comes in
chat.On("addMessage", message => Console.WriteLine(message));
// Start the connection
hubConnection.Start().Wait();
string line = null;
while((line = Console.ReadLine()) != null)
{
// Send a message to the server
chat.Invoke("Send", line).Wait();
}
}
}
Ref: https://github.com/SignalR/SignalR/wiki/QuickStart-Hubs
If there any specific reason you want to use WCF? you can write your service as SignarR hub only.

Resources