Is it possible to change MVC routes after startup? - asp.net-core-webapi

I have an .NET Core 2.1 Web API that registers custom routes on startup via the MapRoute method of IRouteBuilder. This occurs in the Configure() method of startup.
Sometimes these routes need to change and I would like to prevent having to restart the web api in order to add or remove a custom routes. Is it possible to modify the route table while the web site is running?

For custom router dynamicly, you could try IRouter.
Here is a simple demo to change router at runtime from appsettings.json file.
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"Router": {
"Controller": "Home",
"Action": "Contact"
}
}
Custom Router RouterFromAppSettings
public class RouterFromAppSettings : IRouter
{
private readonly IRouter _defaulRouter;
private readonly IConfiguration _config;
public RouterFromAppSettings(IRouter defaulRouter
, IConfiguration config)
{
_defaulRouter = defaulRouter;
_config = config;
}
public async Task RouteAsync(RouteContext context)
{
var controller = _config.GetSection("Router").GetValue<string>("Controller");
var action = _config.GetSection("Router").GetValue<string>("Action");
context.RouteData.Values["controller"] = controller;
context.RouteData.Values["action"] = action;
await _defaulRouter.RouteAsync(context);
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return _defaulRouter.GetVirtualPath(context);
}
}
Register custom route
app.UseMvc(routes =>
{
routes.Routes.Insert(0, new RouterFromAppSettings(routes.DefaultHandler,Configuration));
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
After launch the appplication, you could change the appsettings Router node to redirect to different action.

Related

ASP.NET Core on request before started middleware

I have an asp.net core application and I need to add elastic logging, I decided to use Serilog for it, but I need to add a Correlation id into logs messages, I can't do it within only HTTP correlation id header because I have service bus handlers that also need to have correlation id. So I did it with default asp request middleware, but still have logs without it
request finished/started logs aren't have correlation id
here is my serilog setup
hostBuilder.UseSerilog(
(_, _, loggerConfiguration) =>
{
loggerConfiguration
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.Elasticsearch(ConfigureElasticSink(configuration, environment))
.ReadFrom.Configuration(configuration);
});
and request middleware
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context, ICorrelationIdService correlationIdService)
{
context.Request.Headers.TryGetValue(Constants.CORRELATION_ID_HEADER_NAME, out var correlationId);
correlationIdService.Set(correlationId);
context.Request.Headers.TryAdd(Constants.CORRELATION_ID_HEADER_NAME, correlationIdService.CorrelationId);
LogContext.PushProperty(Constants.CORRELATION_ID, correlationIdService.CorrelationId);
await _next(context);
}
}
UDP
My startup file
using IM360.Logger;
using InMarket360EmailServiceWebApi.WebUI;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLoggerServices();
builder.Services.AddWebUIServices();
builder.Host.UseExternalLogging();
var app = builder.Build();
app.UseExternalLogging(); //middleware being added in this extension method
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
app.MapFallbackToFile("index.html");
app.Run();
public partial class Program { }
Logger extenstions file
public static IServiceCollection AddLoggerServices(this IServiceCollection serviceCollection)
{
serviceCollection.AddScoped<ICorrelationIdService, CorrelationIdService>();
serviceCollection.AddHeaderPropagation(opt => opt.Headers.Add(Constants.CORRELATION_ID_HEADER_NAME));
return serviceCollection;
}
public static WebApplication UseExternalLogging(this WebApplication webApplication)
{
webApplication.UseMiddleware<CorrelationIdMiddleware>();
webApplication.UseHeaderPropagation();
return webApplication;
}
Have any ideas?

How to call custom authentication library method in Azure Function Startup?

I am trying to create an Azure Function (using NET 6.0 and Azure Functions runtime 4.0) and want to use a custom authentication library method AddAzureAdAuthentication that exists in my shared library just like how I was able to use it in this .Net Core api to authenticate api endpoints:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAzureAdAuthentication(Configuration);
services.AddCorsConfiguration(Configuration);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseCors("AllowSpecificOrigin");
app.UseAuthentication();
app.UseMvc();
}
}
I have read many articles and SO posts, but none gave a working solution to this problem. This is what I tried so far -
public override void Configure(IFunctionsHostBuilder builder)
{
var executionContextOptions = builder.Services.BuildServiceProvider()
.GetService<IOptions<ExecutionContextOptions>>().Value;
var currentDirectory = executionContextOptions.AppDirectory;
// Get the original configuration provider from the Azure Function
var configuration = builder.Services.BuildServiceProvider().GetService<IConfiguration>();
// Create a new IConfigurationRoot and add our configuration along with Azure's original configuration
Configuration = new ConfigurationBuilder()
.SetBasePath(currentDirectory)
.AddConfiguration(configuration) // Add the original function configuration
.AddJsonFile("local.settings.json", optional: false, reloadOnChange: true)
.Build();
// Replace the Azure Function configuration with our new one
builder.Services.AddSingleton(Configuration);
ConfigureServices(builder.Services);
}
private void ConfigureServices(IServiceCollection services)
{
services.AddAzureAdAuthentication(Configuration.GetSection("AzureAd"));
}
and my HttpTriggered Azure Function is a default function:
[FunctionName("TestADFunction")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
string responseMessage = string.IsNullOrEmpty(name)
? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
: $"Hello, {name}. This HTTP triggered function executed successfully.";
return new OkObjectResult(responseMessage);
}
When I hit the function, it runs always without authenticating. I am not sure how to make this call to UseAuthentication();. I guess that is what is missing. Or, is there any other way to authenticate and authorize my azure function using my custom auth library?
To achieve the above requirements in your Function app , Need to add nuget package called Microsoft.Identity.Web
And in startup.cs we can use the below sample code:
[assembly: FunctionsStartup(typeof(FunctionsAuthentication.Startup))]
namespace FunctionsAuthentication
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
// This is configuration from environment variables, settings.json etc.
var configuration = builder.GetContext().Configuration;
builder.Services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = "Bearer";
sharedOptions.DefaultChallengeScheme = "Bearer";
})
.AddMicrosoftIdentityWebApi(configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
}
}
}
And in local settings.json we need to provide the credentials of our Azure AD
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
"AzureAd:Instance": "https://login.microsoftonline.com/",
"AzureAd:Domain": "<your_domain>",
"AzureAd:TenantId": "<your_tenant_id>",
"AzureAd:ClientId": "<client_id>",
"AzureAd:ClientSecret": "<client_secret>"
}
}
For complete setup please refer to this Blog

Get the requesting user in controller method of ASP.NET app

I am currently working on an ASP.NET Web app with angular as a front end. As a base, the new template present in VisualStudio 2019 for ASP.NET angular, with Individual Authentication.
This runs on dotnet core 3.0 Preview 4.
First a user is created through the register interface of the template application. Then when a request is made to a controller of the backend, I would like to get the ApplicationUser that made the request.
Is that possible? Do I need to put any sort of token in the header of the http request in the frontend? Do I need to do anything special in the backend?
Currently, the http request in the controller looks like this.
import { Component, Inject } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Router } from "#angular/router";
import { error } from 'protractor';
#Component({
selector: 'app-classes-component',
templateUrl: './classes.component.html'
})
export class ClassesComponent {
public classes: Class[];
public http: HttpClient;
public baseUrl: string;
public courseCodeValue: string;
constructor(http: HttpClient, #Inject('BASE_URL') baseUrl: string, private router: Router) {
this.http = http;
this.baseUrl = baseUrl;
this.refreshCourses();
}
public refreshCourses() {
this.http.get<Class[]>(this.baseUrl + 'api/Courses/GetCourses').subscribe(result => {
this.classes = result;
}, error => console.error(error));
}
}
The Controller looks like this:
[Authorize]
[Route("api/[controller]")]
public class CoursesController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
public CoursesController(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
[HttpGet("[action]")]
public IEnumerable<CourseDto> GetCourses()
{
var user = _userManager.GetUserAsync(User).Result;
// Here the user is null
return user.Courses.Select(item => new CourseDto
{
CourseCode = item.CourseCode,
CurrentGrade = item.CurrentGrade
});
}
}
The issue is that when I try to get the user that is making the http request with the usermanager, I get null. So I was wondering if I was missing something. Like prehaps some sort of token in the header of the request? Am I doing something wrong on the controller side?
EDIT: here is the Startup.cs 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.AddMvc();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddMvc(options => options.EnableEndpointRouting = false)
.AddNewtonsoftJson();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
}
// 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.UseDatabaseErrorPage();
}
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();
app.UseSpaStaticFiles();
app.UseAuthentication();
app.UseIdentityServer();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{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");
}
});
}
}
You can use "User.Identity.Name" to get the UserId of the User making the request then pass it to FindByIdAsync().
var user = await _userManager.FindByIdAsync(User.Identity.Name);
or a db hit against the UserId(User.Identity.Name);
which ever works best for you.
Please let me know if this works.

No file provider has been configured to process the supplied file

I am using Single Page Application with .Net core 2.2. I have following in my startup.cs.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
routes.MapSpaFallbackRoute(
name: "spa - fallback",
defaults: new { controller = "CatchAll", action = "Index" });
});
But having following
public class CatchAllController : Controller
{
public IActionResult Index()
{
return File("~/index.html", "text/html");
}
}
gives me following error.
No file provider has been configured to process the supplied file
I was following following article (one difference. article uses API project. I had taken angular project).
https://www.richard-banks.org/2018/11/securing-vue-with-identityserver-part1.html
I was just fighting this exact problem. Even though the File() method claims to take a "virtual path" I could never get it to load the file without the error you're getting. Here's what I ended up piecing together after reading a number of different posts.
[Route("[controller]")]
public class DocumentationController : ControllerBase
{
private readonly IHostingEnvironment _env;
public DocumentationController(IHostingEnvironment env)
{
_env = env;
}
[HttpGet, Route("apiReference")]
public IActionResult ApiReference()
{
var filePath = Path.Combine(_env.ContentRootPath,
"Controllers/Documentation/My Document.pdf");
return PhysicalFile(filePath, "application/pdf");
}
}

Unable to resolve service for type 'MyApp.ApplicationDbContext' while attempting to activate

I am having issue after integrated in an existing ASP.Net core 2.0 application a newly created authentication project items based on ASP.Net Core 2.0 too.
after some adjustments then successful build, in the browsser I got the next error:
An unhandled exception occurred while processing the request.
InvalidOperationException: Unable to resolve service for type 'SpaServices.ApplicationDbContext' while attempting to activate
The authentication has to use a separate database which is not yet scaffolded
My Startup.cs is as follows:
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/Account/Manage");
options.Conventions.AuthorizePage("/Account/Logout");
});
// Register no-op EmailSender used by account confirmation and password reset during development
// For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=532713
services.AddSingleton<IEmailSender, EmailSender>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
#region webpack-middleware-registration
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
// Call UseWebpackDevMiddleware before UseStaticFiles
app.UseStaticFiles();
#endregion
#region mvc-routing-table
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
#endregion
}
And my constructor is as below:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
}
I have updated the constructor to explicitly expect a type the container knows how to resolve:
public ApplicationDbContext(DbContextOptions options)
: base(options)
{
}
Without success, any thoughts ?
You need to include the following in your ConfigureServices() method:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("[Your Connection String Identifier]")));
Currently, you have only added a "SchoolContext" DbContext

Resources