I was learing about HealthChecks following the information posted on the MSDN site: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-5.0#register-health-check-services
The following code snippet tells that if an unhealthy checks returns I can overwrite the value by saying it's degraded instead.
services.AddHealthChecks()
.AddCheck<ExampleHealthCheck>(
"example_health_check",
failureStatus: HealthStatus.Degraded,
tags: new[] { "example" });
So I tried this with this implementation, assuming that I would get a degraded result instead of an unhealthy one:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHealthChecks().AddCheck<ExampleHealthCheck>("ExampleHealthCheck", failureStatus: HealthStatus.Degraded);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health");
});
}
}
internal class ExampleHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(HealthCheckResult.Unhealthy("An unhealthy result."));
}
}
Can someone shed some light on why this isn't working or where I got it wrong?
It is quite confusing, because you expect that the HealthCheck gives you back automatically the result you defined on the failureStatus property, but your HealthCheck explicitly returns Unhealthy.
If you take a look at the AddHealthCheck<T> method on GitHub, you will see in only creates an instance of HealthCheckRegistration.
public static IHealthChecksBuilder AddCheck(
this IHealthChecksBuilder builder,
string name,
IHealthCheck instance,
HealthStatus? failureStatus = null,
IEnumerable<string>? tags = null,
TimeSpan? timeout = null)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
return builder.Add(new HealthCheckRegistration(name, instance, failureStatus, tags, timeout));
}
This object is afterwards passed inside the context of your health check. You can then read the failureStatus which is expected in case of failure and then return an HealthCheckResult from it. In your case, this would return Degraded :
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHealthChecks().AddCheck<ExampleHealthCheck>("ExampleHealthCheck", failureStatus: HealthStatus.Degraded);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health");
});
}
}
internal class ExampleHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus));
}
}
Related
My goal is to make the communication between two applications (WebAPI and Worker) via MassTransit's Request/Response technique. The problem is that I'm never getting inside the consumer (request client), I'm getting a timeout instead.
I found a similar question already but the answer included a link to a github repository which no longer exists. I also tried following a sample but for some reason most samples are created as console applications which is useless for me since I have two WebAPIs trying to communicate with each other.
Anyway, here's my code:
WebAPI.Startup
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddMassTransit(massTransitConfig =>
{
massTransitConfig.UsingAzureServiceBus((ctx, cfg) =>
{
cfg.Host("Endpoint=sb://----.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=----");
});
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Worker.Startup
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddMassTransit(massTransitConfig =>
{
massTransitConfig.UsingAzureServiceBus((ctx, cfg) =>
{
cfg.Host("Endpoint=sb://----.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=----");
});
massTransitConfig.AddConsumer<CreateScheduleRequestClient>();
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
WebAPI.RequestController
[Route("api/requests")]
public class RequestsController : ControllerBase
{
private readonly IBus _bus;
public RequestsController(IBus bus)
{
_bus = bus;
}
[HttpPost("create-schedule")]
public async Task<IActionResult> CreateSchedule()
{
var client = _bus.CreateRequestClient<CreateScheduleRequest>();
var response = await client.GetResponse<ScheduleCreatedResponse>(new CreateScheduleRequest());
return Ok(response.Message.Succeeded);
}
}
DataTransferObjects.CreateScheduleRequest
public class CreateScheduleRequest
{
public string CommandName { get; set; }
public string Cron { get; set; }
}
Worker.RequestClients.CreateScheduleRequestClient
public class CreateScheduleRequestClient : IConsumer<CreateScheduleRequest>
{
public async Task Consume(ConsumeContext<CreateScheduleRequest> context)
{
await context.RespondAsync(new ScheduleCreatedResponse(true));
}
}
DataTransferObjects.ScheduleCreatedResponse
public class ScheduleCreatedResponse
{
public bool Succeeded { get; }
public ScheduleCreatedResponse(bool succeeded)
{
Succeeded = succeeded;
}
}
When I call the only endpoint in the RequestsController, I'm getting MassTransit.RequestTimeoutException: Timeout waiting for response, RequestId: 27f60000-167b-00ff-ea0f-08d8e3a0832e after a short period. I'm not able to verify much more about it, I thought it outght to work out of the box but perhaps I'm missing some parameters when initializing the bus in my Startup classes?
================EDIT================
I changed my code a little with regards to what Chris Patterson suggested and to specify that I'd like to go with the IBus approach. The code still throws the same exception after the change.
First, I'd suggest reviewing the request documentation, in particular the controller example that injects IRequestClient<T> into the controller. That will fix your controller code, which shouldn't be using IBus.
Second, your response should be an actual message type, it can't be true. You need to create a second message contract, such as ScheduleRequestCreated and respond with that message type. Then, your GetResponse would change to
GetResponse<ScheduleRequestCreated>(new CreateScheduleRequest(...))
And your response would be:
RespondAsync(new ScheduleRequestCreated(...))
I already have been trying to solve this for days, I created a Web API application (using .NET 5, VS2019) and when I was finally be able to run it without errors in the IIS, I receive 404 errors in the responses, simply put the controller code is never called (never hit by the debugger) with no exceptions or any other errors, just this screen (calling the first route in the controller):
This localhost page can’t be found
No webpage was found for the web address:
https://localhost:44342/sicrestweb/sync/gs/usuarios?json_data={fields:*,%20database:"Provider=Microsoft.ACE.OLEDB.12.0;Data%20Source=F:\VBDev\Sicrest3.4\DPWT314.mdb;%20Persist%20Security%20Info=False;"}
Here is the Program code:
public static void Main(string[] args)
{
CD = Directory.GetCurrentDirectory();
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
The Startup code:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
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.AddControllers();
services.AddScoped<ISyncService, SyncService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); }
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
The controller code (fragment) :
[ApiController]
[Route("SicrestWeb/[controller]")]
public class SyncController : ControllerBase // Controller
{
private readonly ISyncService ISS;
private SyncService SS { get { return (SyncService) ISS; } }
public ActionResult HTTPResponse { get { return SS.HTTPResponse; } } // HTTP Response
public String CurrentRoute { get { return (Request != null && Request.Path != null) ? (Request.Path.Value ?? TestRoute) :TestRoute; } } // Get route string
public String TestRoute { get; set; }
public SyncController(ISyncService isys) { ISS = isys; TestRoute = ""; }
[HttpGet]
[Route("GS/{table_name}/{json_data}/{key=''}")]
[Route("GSI/{table_name}/{json_data}/{key=''}")]
public ActionResult Select(String table_name, String json_data, String key="") // (GS) returns data as requested
{
// process request
if (!ISS.Dispatch(CurrentRoute, table_name, json_data, key))
{
// Handles error
}
// Generate response
return HTTPResponse;
}
.
.
.
}
The only things I am sure is the controller code works when is called directly (it is already unit tested), but the address/routing mechanism never calls it, like the controller and its routes are not identified thus are never called.
We're making an ASP.NET Core API for a web app that should get a list of users (and expand a specific field) from a SQL Server database with Entity Framework. It works until we specify which user Id we want in the URL.
Here is the Startup class:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<User>("Users");
builder.EntitySet<Character>("Characters");
return builder.GetEdmModel();
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<GaiumContext>(opts => opts.UseSqlServer(Configuration["ConnectionString:Gaium"]));
services.AddOData();
services.AddMvc(options =>
{
options.EnableEndpointRouting = false;
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// 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
{
// 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.UseMvc(b =>
{
b.EnableDependencyInjection();
b.Select().Expand().Filter().OrderBy().MaxTop(100).Count();
b.MapODataServiceRoute("api", "api", GetEdmModel());
});
}
}
Here is the DbContext:
public class GaiumContext : DbContext
{
public GaiumContext(DbContextOptions<GaiumContext> options) : base(options)
{
}
public DbSet<User> Users { get; set; }
public DbSet<Character> Characters { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().HasMany(c => c.Characters);
}
}
Finally, the controller UsersController :
public class UsersController : ODataController
{
private GaiumContext _context;
public UsersController(GaiumContext context)
{
_context = context;
}
[EnableQuery]
public IActionResult Get()
{
return Ok(_context.Users);
}
[EnableQuery]
public IActionResult Get(long key)
{
return Ok(_context.Users.Find(key));
}
}
The user object looks like this:
Users {
id: int,
name: string,
Characters: [{
id: int,
name: String
}]
}
Here's a query for all the users:
GET: https://localhost:44323/api/users?$expand=Characters
In this case the query works fine and we do receive the list of users, as well as their Characters field.
{"#odata.context":"https://localhost:44323/api/$metadata#Users","value":[{"Id":1,"Username":"Ok","Characters":[{"Id":1,"Name":"bigusernamesmoke"}]}]}
But when we try to get the result for one specific user, using their ID, the Characters list is empty:
GET: https://localhost:44323/api/users/1?$expand=Characters
{"#odata.context":"https://localhost:44323/api/$metadata#Users/$entity","Id":1,"Username":"Ok","Characters":[]}
Note the Find(key) method returns a single Book instead of a queryable object :
return Ok(_context.Users.Find(key));
If I change your code to return a SingleResult as below:
[EnableQuery]
public SingleResult<User> Get(long key)
{
var query = _context.Users.Where(p => p.Id == key);
return SingleResult.Create(query);
}
it works fine for me.
When I try to run my app I get the error
InvalidOperationException: Cannot resolve 'API.Domain.Data.Repositories.IEmailRepository' from root provider because it requires scoped service 'API.Domain.Data.EmailRouterContext'.
What's odd is that this EmailRepository and interface is set up exactly the same as far as I can tell as all of my other repositories yet no error is thrown for them. The error only occurs if I try to use the app.UseEmailingExceptionHandling(); line. Here's some of my Startup.cs file.
public class Startup
{
public IConfiguration Configuration { get; protected set; }
private APIEnvironment _environment { get; set; }
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
Configuration = configuration;
_environment = APIEnvironment.Development;
if (env.IsProduction()) _environment = APIEnvironment.Production;
if (env.IsStaging()) _environment = APIEnvironment.Staging;
}
public void ConfigureServices(IServiceCollection services)
{
var dataConnect = new DataConnect(_environment);
services.AddDbContext<GeneralInfoContext>(opt => opt.UseSqlServer(dataConnect.GetConnectString(Database.GeneralInfo)));
services.AddDbContext<EmailRouterContext>(opt => opt.UseSqlServer(dataConnect.GetConnectString(Database.EmailRouter)));
services.AddWebEncoders();
services.AddMvc();
services.AddScoped<IGenInfoNoteRepository, GenInfoNoteRepository>();
services.AddScoped<IEventLogRepository, EventLogRepository>();
services.AddScoped<IStateRepository, StateRepository>();
services.AddScoped<IEmailRepository, EmailRepository>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
app.UseAuthentication();
app.UseStatusCodePages();
app.UseEmailingExceptionHandling();
app.UseMvcWithDefaultRoute();
}
}
Here is the EmailRepository
public interface IEmailRepository
{
void SendEmail(Email email);
}
public class EmailRepository : IEmailRepository, IDisposable
{
private bool disposed;
private readonly EmailRouterContext edc;
public EmailRepository(EmailRouterContext emailRouterContext)
{
edc = emailRouterContext;
}
public void SendEmail(Email email)
{
edc.EmailMessages.Add(new EmailMessages
{
DateAdded = DateTime.Now,
FromAddress = email.FromAddress,
MailFormat = email.Format,
MessageBody = email.Body,
SubjectLine = email.Subject,
ToAddress = email.ToAddress
});
edc.SaveChanges();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
edc.Dispose();
disposed = true;
}
}
}
And finally the exception handling middleware
public class ExceptionHandlingMiddleware
{
private const string ErrorEmailAddress = "errors#ourdomain.com";
private readonly IEmailRepository _emailRepository;
private readonly RequestDelegate _next;
public ExceptionHandlingMiddleware(RequestDelegate next, IEmailRepository emailRepository)
{
_next = next;
_emailRepository = emailRepository;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next.Invoke(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex, _emailRepository);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception,
IEmailRepository emailRepository)
{
var code = HttpStatusCode.InternalServerError; // 500 if unexpected
var email = new Email
{
Body = exception.Message,
FromAddress = ErrorEmailAddress,
Subject = "API Error",
ToAddress = ErrorEmailAddress
};
emailRepository.SendEmail(email);
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int) code;
return context.Response.WriteAsync("An error occured.");
}
}
public static class AppErrorHandlingExtensions
{
public static IApplicationBuilder UseEmailingExceptionHandling(this IApplicationBuilder app)
{
if (app == null)
throw new ArgumentNullException(nameof(app));
return app.UseMiddleware<ExceptionHandlingMiddleware>();
}
}
Update:
I found this link https://github.com/aspnet/DependencyInjection/issues/578 which led me to change my Program.cs file's BuildWebHost method from this
public static IWebHost BuildWebHost(string[] args)
{
return WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
to this
public static IWebHost BuildWebHost(string[] args)
{
return WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseDefaultServiceProvider(options =>
options.ValidateScopes = false)
.Build();
}
I don't know what exactly is going on but it seems to work now.
You registered the IEmailRepository as a scoped service, in the Startup class.
This means that you can not inject it as a constructor parameter in Middleware because only Singleton services can be resolved by constructor injection in Middleware. You should move the dependency to the Invoke method like this:
public ExceptionHandlingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context, IEmailRepository emailRepository)
{
try
{
await _next.Invoke(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex, emailRepository);
}
}
Another way to get the instance of scoped dependency is to inject service provider (IServiceProvider) into the middleware constructor, create scope in Invoke method and then get the required service from the scope:
using (var scope = _serviceProvider.CreateScope()) {
var _emailRepository = scope.ServiceProvider.GetRequiredService<IEmailRepository>();
//do your stuff....
}
Check out Resolving Services in a Method Body in asp.net core dependency injection best practices tips tricks for more details.
Middleware is always a singleton so you can't have scoped dependencies as constructor dependencies in the constructor of your middleware.
Middleware supports method injection on the Invoke method,so you can just add the IEmailRepository emailRepository as a parameter to that method and it will be injected there and will be fine as scoped.
public async Task Invoke(HttpContext context, IEmailRepository emailRepository)
{
....
}
Your middleware and the service has to be compatible with each other in order to inject the service via the constructor of your middleware. Here, your middleware has been created as a convention-based middleware which means it acts as a singleton service and you have created your service as scoped-service. So, you cannot inject a scoped-service into the constructor of a singleton-service because it forces the scoped-service to act as a singleton one. However, here are your options.
Inject your service as a parameter to the InvokeAsync method.
Make your service a singleton one, if possible.
Transform your middleware to a factory-based one.
A Factory-based middleware is able to act as a scoped-service. So, you can inject another scoped-service via the constructor of that middleware. Below, I have shown you how to create a factory-based middleware.
This is only for demonstration. So, I have removed all the other code.
public class Startup
{
public Startup()
{
}
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<TestMiddleware>();
services.AddScoped<TestService>();
}
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<TestMiddleware>();
}
}
The TestMiddleware:
public class TestMiddleware : IMiddleware
{
public TestMiddleware(TestService testService)
{
}
public Task InvokeAsync(HttpContext context, RequestDelegate next)
{
return next.Invoke(context);
}
}
The TestService:
public class TestService
{
}
In .NET Core 6, the below settings worked for me.
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider.GetRequiredService<IDbInitilizer>;
services.Invoke().Initialize();
}
I am attempting to implement the API Key Validator mentioned in this post. I am running into an issue where the injected service I am using to do validation in the middleware class is returning:
InvalidOperationException: Cannot resolve 'FoosballKeepr.Services.Interfaces.ILeagueService' from root provider because it requires scoped service 'FoosballKeepr.Data.FoosballKeeprContext'.
I believe I am registering my dbContext, services, and repositories correctly in Startup.cs.
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
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)
{
//MVC
services.AddMvc();
//Database
var connection = #"Server=localhost\SQLEXPRESS;Database=FoosballKeepr;Trusted_Connection=True;";
services.AddDbContext<FoosballKeeprContext>(options => options.UseSqlServer(connection));
//Services
services.AddTransient<IPlayerService, PlayerService>();
services.AddTransient<ILeagueService, LeagueService>();
//Repositories
services.AddTransient<IPlayerRepository, PlayerRepository>();
services.AddTransient<ILeagueRepository, LeagueRepository>();
}
// 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();
}
app.UseMiddleware<ApiKeyValidatorMiddleware>();
app.UseMvc();
}
}
Custom middleware validator:
public class ApiKeyValidatorMiddleware
{
private readonly RequestDelegate _next;
private ILeagueService _leagueService;
public ApiKeyValidatorMiddleware(RequestDelegate next, ILeagueService leagueService)
{
_next = next;
_leagueService = leagueService;
}
public async Task Invoke(HttpContext context)
{
if (!context.Request.Headers.Keys.Contains("x-api-key"))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("API Key Missing.");
return;
}
else
{
int leagueId = _leagueService.ValidateApiKey(context.Request.Headers["x-api-key"]);
if (leagueId == 0)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Invalid API Key");
return;
}
else
{
context.Items["LeagueId"] = leagueId;
}
}
await _next.Invoke(context);
}
}
Service
public class LeagueService : ILeagueService
{
private readonly ILeagueRepository _leagueRepository;
public LeagueService(ILeagueRepository leagueRepository)
{
_leagueRepository = leagueRepository;
}
public int ValidateApiKey(string apiKey)
{
return _leagueRepository.ValidateApiKey(apiKey);
}
}
Repository
public class LeagueRepository : ILeagueRepository
{
private readonly FoosballKeeprContext _context;
public LeagueRepository(FoosballKeeprContext context)
{
_context = context;
}
public int ValidateApiKey(string apiKey)
{
var query = from l in _context.League
where l.ApiKey == apiKey
select l.LeagueId;
return query.FirstOrDefault();
}
}
This is my first time implementing custom middleware functionality so I feel like my issue is not correctly setting something up in the correct context, but nothing is popping up as obvious. Does this look familiar to anyone??
The problem is that middlewares don't have a scope, given that:
Middleware is constructed once per application lifetime
So, when you need to inject scoped services, you do it at the Invoke operation (what's known as method injection):
public async Task Invoke(HttpContext context, ILeagueService service)
{
//...
}