I have this Configure method in my Startup.cs.
It does 3 things :
serve static files under wwwroot
add CSP header for index.html
serve parameters via /settings.json route
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
// Defaults to index.html
var defaultFilesOptions = new DefaultFilesOptions();
defaultFilesOptions.DefaultFileNames.Clear();
defaultFilesOptions.DefaultFileNames.Add("index.html");
app.UseDefaultFiles(defaultFilesOptions);
var staticFileOptions = new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// Add CSP for index.html
if (ctx.File.Name == "index.html")
{
ctx.Context.Response.Headers.Append(
"Content-Security-Policy", "default-src 'self'" // etc
);
}
}
};
app.UseStaticFiles(staticFileOptions); // wwwroot
app.UseRouting();
app.UseEndpoints(endpoints =>
{
// Settings.json endpoint
endpoints.MapGet("/settings.json", async context =>
{
string json = $#"
{{
""myConfig"": ""{_configuration["myParameter"]}""
}}";
await context.Response.WriteAsync(json);
});
});
}
}
The files under wwwroot are in fact a vue.js app with routing. I need to return index.html for all inexistant request, for the client routing to take control of the page.
Currently it returns a 404 and does not pass in OnPrepareResponse hook.
How can I configure index fallback for the router to work in history mode ?
I think it is acheivable by configuration in web.config, but I'd prefer to configure this in Startup.js so this is all in the same place.
I ended up writing a file provider that does the index fallback. It encapsulate a PhysicalFileProvider, and return index.html under certain conditions if the file is not found. In my case conditions are based on folders css, img or js.
It is implemented this way :
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using System.Linq;
public class IndexFallbackFileProvider : IFileProvider
{
private readonly PhysicalFileProvider _innerProvider;
public IndexFallbackFileProvider(PhysicalFileProvider physicalFileProvider)
{
_innerProvider = physicalFileProvider;
}
public IDirectoryContents GetDirectoryContents(string subpath)
{
return _innerProvider.GetDirectoryContents(subpath);
}
public IFileInfo GetFileInfo(string subpath)
{
var fileInfo = _innerProvider.GetFileInfo(subpath);
if(!fileInfo.Exists && MustFallbackToIndex(subpath))
{
if(!_staticFilesFolders.Any(f => subpath.Contains(f)))
{
fileInfo = _innerProvider.GetFileInfo("/index.html");
}
}
return fileInfo;
}
// Plain 404 are OK for css, img, js.
private static string[] _staticFilesFolders = new string[] { "/css/", "/img/", "/js/" };
private static bool MustFallbackToIndex(string subpath)
{
return !_staticFilesFolders.Any(f => subpath.Contains(f));
}
public IChangeToken Watch(string filter)
{
return _innerProvider.Watch(filter);
}
}
Then, in startup.config, I use this provider.
Plus, I had to set ServeUnknownFileTypes to true to respond to requests to a /path/without/extension.
var physicalFileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "wwwroot"));
var fileProvider = new IndexFallbackFileProvider(physicalFileProvider);
var staticFileOptions = new StaticFileOptions
{
FileProvider = fileProvider,
ServeUnknownFileTypes = true
};
app.UseStaticFiles(staticFileOptions);
Related
I've found that in the application I've been set to work on, there's a TelemetryInitialier which is called at every web api call and adds some properties from the appsettings.json.
Here's the defined class
public class AppInsightTelemetryInitializer : ITelemetryInitializer
{
private readonly AppInsightsMetrics _appInsightsMetrics;
public AppInsightTelemetryInitializer(IOptions<AppInsightsMetrics> appInsightsMetrics)
{
_appInsightsMetrics = appInsightsMetrics.Value;
}
public void Initialize(ITelemetry telemetry)
{
var propTelemetry = (ISupportProperties)telemetry;
propTelemetry.Properties["Application"] = _appInsightsMetrics.Application;
propTelemetry.Properties["Version"] = _appInsightsMetrics.Version;
propTelemetry.Properties["LatestCommit"] = _appInsightsMetrics.LatestCommit;
}
}
This class is registered this way
appBuilder.Services.Configure<AppInsightsMetrics>(appBuilder.Configuration.GetSection(AppInsightsMetrics.InsightsMetrics));
appBuilder.Services.AddSingleton<ITelemetryInitializer, AppInsightTelemetryInitializer>();
if (env.IsDevelopment())
{
services.AddApplicationInsightsTelemetry(x =>
{
x.EnableDebugLogger = false;
x.InstrumentationKey = "instrumentation key";
});
}
else
{
services.AddApplicationInsightsTelemetry();
}
And the data are loaded from the appsettings.json file as
"AppInsightsMetrics": {
"Application": "xxx.Api",
"Version": "",
"LatestCommit": ""
},
Those data are replaced in production by CI/CD azure pipeline.
I was wondering, is there a way of defining them at configuration time and remove this middleware from each call?
Thanks in advance
I upgraded the source code to .NET Core v3.1 & I'm having trouble figuring how to debug the backend issue due to lot of dependency injections, abstractions & overriding classes/methods all over. The employee who wrote this have overcomplicate things & he had left the company so we got stuck with the confusing source code mess here that take a lot of our time & energy, to make sense of the it. :-/
The error I'm having is a 401 unauthorize response. I discovered the debugger doesnt respond in StartUp class when consuming the webservice, it only respond when you start up the Web App. So, it took us a while & finally found a hitting debugger breakpoint on a MVC controller page to point us in the right direction. There it is saying the Identity is not authenticated so that explain the unauthorize error.
We're not familiar with this one authentication technology, Odachi. We believe there are 2 seperate authentication architecture, which is ehe WebApp's webpages login authorization for the customer's web browser & Odachi deal with the WebApp's webservice login authorization for the 3rd party software making the webservice call.
Source code below is the webservice MVC controller w/ Authorization filter. Then further down will be the Startup w/ base Startup abstraction.
[ Webservice call ]
namespace ABC.Payments.AspNet.MVC
{
public class AuthorizeWithNoChallengeFilterAttribute : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.User?.Identity.IsAuthenticated != true)
context.Result = new UnauthorizedResult();
}
}
}
namespace ABC.Payments.MerchantWeb.Api
{
[TypeFilter(typeof(AuthorizeWithNoChallengeFilterAttribute))]
public class MerchantsV1Controller : Controller
{
[Route("api/v1/merchants/{merchantAccountId}/customers/payments/paymentmethods"), HttpPost]
public async Task<ObjectResult> Payment([FromBody] ItemInfoViewModel itemInfo, CancellationToken cancellationToken)
{
var payments = whatever();
return new HttpNotAcceptableObjectResult(payments);
}
}
}
[ Startup & Startup Base ]
namespace ABC.Payments.MerchantWeb
{
// StackOverflow Post on how to find 401 Unathorize error (debug)
// --> https://stackoverflow.com/questions/43574552/authorization-in-asp-net-core-always-401-unauthorized-for-authorize-attribute
public class Startup : StartupBase<MerchantRequestContext, Merchant>
{
private const string _schemeCustomMerchantBasic = "CustomMerchantBasic";
public Startup(IWebHostEnvironment webHostEnvironment)
: base(webHostEnvironment, PortalRoleType.Merchant)
{
}
public void ConfigureServices(IServiceCollection services)
{
base._configureServices(true, services);
services.AddTransient(sp => sp.GetService<MerchantRequestContext>()?.Merchant);
services.AddTransient(sp => sp.GetService<MerchantRequestContext>()?.Customer);
services.AddTransient(sp => sp.GetService<MerchantRequestContext>()?.TenantSettings);
services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
services.AddMvcCore()
.AddViewLocalization(LanguageViewLocationExpanderFormat.SubFolder, setup =>
{
setup.ResourcesPath = "Resources";
})
.AddDataAnnotationsLocalization()
.AddApiExplorer();
services.AddCors(options =>
{
options.AddPolicy("Internal", p => p.WithOrigins(base._configuration["Cors:InternalSource"]).WithMethods("POST").WithHeaders("accept", "request", "authorization", "content-type", "internal"));
});
services.AddAuthentication()
// https://github.com/Kukkimonsuta/Odachi/blob/master/src/Odachi.AspNetCore.Authentication.Basic/Events/BasicSignInContext.cs (Basic Sign Context)
// https://github.com/Kukkimonsuta/Odachi/blob/master/samples/BasicAuthenticationSample/Startup.cs
.AddBasic(_schemeCustomMerchantBasic, options =>
{
// ////////Notice: AutomaticChallenge is depreciated, google search said to use DefaultChallengeScheme w/ given cookie-authentication-scheme but that still doesnt explain how to disable it
// //////// https://stackoverflow.com/questions/45878166/asp-net-core-2-0-disable-automatic-challenge
// //////// https://github.com/dotnet/aspnetcore/issues/2007
//## options.AutomaticChallenge = false;
options.Realm = "AutoPayment API v1";
options.Events = new BasicEvents()
{
OnSignIn = async context =>
{
var claims = new List<Claim>();
if (context.Username == "ndi3DanDba993nvbaqbn3d93" && context.Password == "aVd3Ed51dfDE5acCCni9l1IxPq9")
claims.Add(new Claim(ClaimTypes.Role, "InternalAPIUser"));
else
{
string merchantAccountId = context.Request.Path.Value.Split('/').Skip(4).FirstOrDefault();
var merchantRepository = context.HttpContext.RequestServices.GetRequiredService<IMerchantRepository>();
if (merchantAccountId == null || merchantAccountId.Length != 14 || merchantAccountId.Split('-').Length != 3)
throw new Exception($"Invalid merchant account Id ({merchantAccountId ?? string.Empty}).");
var merchant = await merchantRepository.GetMerchantAsync(merchantAccountId, context.HttpContext.RequestAborted);
if (merchant == null || !merchant.IsActive || (merchant.GatePayApiKey != context.Username || merchant.GatePayApiSecret != context.Password))
{
context.Fail("Invalid merchant"); //## context.HandleResponse();
return;
}
}
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); //## options.AuthenticationScheme));
context.Principal = principal;
//## context.Ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), options.AuthenticationScheme);
context.Success(); //## context.HandleResponse();
//return Task.CompletedTask;
}
};
});
}
public void Configure(IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
{
base._configure(true, applicationBuilder, loggerFactory, serviceProvider);
applicationBuilder.UseCors("Internal");
applicationBuilder.UseWhen(context => !context.Request.Path.StartsWithSegments(new PathString("/api/v1")), b => b.UseAuthentication());
}
}
}
namespace ABC.Payments
{
public class StartupBase<TRequestContext, TUserContext>
where TRequestContext : RequestContext<TUserContext>
{
public StartupBase(IWebHostEnvironment webHostEnvironment, PortalRoleType portalRoleType)
{
_portalRoleType = portalRoleType;
_webHostEnvironment = webHostEnvironment;
var builder = new ConfigurationBuilder();
ConfigurationLoader.Load(builder, webHostEnvironment);
_configuration = builder.Build();
if (webHostEnvironment.EnvironmentName.Equals("Production", StringComparison.OrdinalIgnoreCase) == true && _configuration["ConfirmProduction"]?.Equals("Yes", StringComparison.OrdinalIgnoreCase) != true)
throw new Exception("Azure defaults to \"Production\" for the environment, so you need to create an AppSetting of \"ConfirmProduction\" to \"Yes\" to ensure that is the intent.");
}
private readonly IWebHostEnvironment _webHostEnvironment;
public readonly IConfiguration _configuration;
private readonly PortalRoleType _portalRoleType;
public void _configureServices(bool isWebBrowserFrontendGui, IServiceCollection services)
{
if (isWebBrowserFrontendGui)
{
services.AddDistributedRedisCache(options =>
{
options.Configuration = _configuration["Storage:Redis:Configuration"];
});
services.AddSingleton<RedisCache>();
services.AddSingleton<MemoryDistributedCache>();
services.AddSingleton<IDistributedCache>(
sp => new ResilientDistributedCache(sp.GetRequiredService<RedisCache>(), sp.GetRequiredService<MemoryDistributedCache>())
);
var azureBlobConnectionTring = _configuration["Storage:AzureBlob:ConnectionString"];
if (azureBlobConnectionTring != null)
{
var storageAccount = CloudStorageAccount.Parse(azureBlobConnectionTring);
var client = storageAccount.CreateCloudBlobClient();
var azureBlobContainer = client.GetContainerReference("dataprotection-key-container");
services.AddDataProtection().PersistKeysToAzureBlobStorage(azureBlobContainer, "keys.xml");
}
services.AddSession(options =>
{
//options.IdleTimeout = TimeSpan.FromMinutes(5);
});
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole<Guid>>()
.AddEntityFrameworkStores<ApplicationContext>() // FYI - AddEntityFrameworkStores() deal with role that derives from IdentityRole, as per documentation.
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options => {
options.LoginPath = new PathString("/Home/Index");
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(_configuration.GetValue<int?>("Authentication:SlidingExpirationTime").Value);
options.AccessDeniedPath = new PathString("/Home/AccessDenied");
});
services.Configure<IdentityOptions>(options => {
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = false;
options.Password.RequiredLength = 7;
});
services.AddControllersWithViews();
services.AddRazorPages();
// AddMvc() vs AddMvcCore() explaination found at --> https://offering.solutions/blog/articles/2017/02/07/the-difference-between-addmvc-and-addmvccore/
// --> https://stackoverflow.com/questions/42365275/how-to-implement-a-pure-asp-net-core-web-api-by-using-addmvccore/42365276#42365276
services.AddMvc().AddRazorRuntimeCompilation();
services.Configure<MvcRazorRuntimeCompilationOptions>();
services.Configure<AuthorizationOptions>(options =>
{
options.DefaultPolicy = AuthorizationPolicy.Combine(options.DefaultPolicy,
new AuthorizationPolicy(new IAuthorizationRequirement[] {
new RolesAuthorizationRequirement(new string[] { _portalRoleType.ToString(), PortalRoleType.Internal.ToString() }),
new ImpersonationNotExpiredAuthorizationRequirement(_portalRoleType, _configuration.GetValue<TimeSpan?>("Authentication:ImpersonationTimeLimit").Value)
}, new string[0]));
});
services.AddMvcCore(options =>
{
var requestContextAttribute = new LoadRequestContextAttribute(typeof(TRequestContext));
options.Filters.Add(requestContextAttribute);
options.ModelBinderProviders[options.ModelBinderProviders.IndexOf(
options.ModelBinderProviders.OfType<ComplexTypeModelBinderProvider>().First()
)] = new TryServicesModelBinderProvider(services.BuildServiceProvider());
options.ModelBinderProviders.Insert(0, new EnumModelBinderProvider(services.BuildServiceProvider()));
})
.AddDataAnnotationsLocalization()
.AddNewtonsoftJson(settings =>
{
settings.SerializerSettings.ContractResolver = new DefaultContractResolver();
});
services.Configure<ForwardedHeadersOptions>(options => options.RequireHeaderSymmetry = false);
}
//services.AddPayments<TRequestContext, TUserContext>(_configuration, string.Empty);
}
public void _configure(bool isWebBrowserFrontendGui, IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
{
if (isWebBrowserFrontendGui)
{
serviceProvider.GetRequiredService<ITelemeter<StartupBase>>().TrackMetric("Startup Time", (DateTime.UtcNow - DateTime.UtcNow).TotalSeconds);
// Exception Page Handling.
if (!_webHostEnvironment.IsProduction())
{
applicationBuilder.UseDeveloperExceptionPage();
//applicationBuilder.UseDatabaseErrorPage();
}
else
applicationBuilder.UseExceptionHandler("/Home/ErrorPage.html");
applicationBuilder.UseStaticFiles(); // Note, we are not authenticating for static files if this is before them
//applicationBuilder.UseStatusCodePages();
// Session.
applicationBuilder.UseSession();
applicationBuilder.UseAuthentication();
// Routing.
applicationBuilder.UseRouting();
applicationBuilder.UseAuthorization(); // Exception error said to put this between UseRouting() & UseEnpoint().
applicationBuilder.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
// Config Localization.
var options = serviceProvider.GetService<IOptions<RequestLocalizationOptions>>();
if (options != null)
applicationBuilder.UseRequestLocalization(options.Value);
applicationBuilder.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All });
// Ensure Https.
var portals = applicationBuilder.ApplicationServices.GetRequiredService<Portals>();
applicationBuilder.Use(next => async httpContext =>
{
if (httpContext.Request.Host.Value.Contains("localhost"))
{
await next(httpContext);
}
else
{
string host = portals.GetHostForRedirect(httpContext.Request.Host.Value);
if (!host.Equals((httpContext.Request.IsHttps ? "https://" : "http://") + httpContext.Request.Host, StringComparison.OrdinalIgnoreCase))
httpContext.Response.Redirect($"{host}{httpContext.Request.Path}{httpContext.Request.QueryString}");
else
await next(httpContext);
}
});
}
//applicationBuilder.UsePayments<TRequestContext, TUserContext>();
}
}
}
I'm writing a .net core 2.2 MVC app, using NLog.
from the Main function, the app writes to all levels of NLog, but from the controllers, nothing is being written.
Logging section in appsettings.json:
"Logging": {
"LogLevel": {
"Default": "Trace",
"Microsoft": "Information"
}
}
Nlog Config:
public static class NlogConfig
{
public static LoggingConfiguration GetLoggingConfiguration()
{
var logConfig = new LoggingConfiguration();
var consoleTarget = new ColoredConsoleTarget("consoleTarget")
{
Layout = #"${date:format=HH\:mm\:ss} ${level} ${message} ${exception}"
};
logConfig.AddTarget(consoleTarget);
logConfig.AddRuleForAllLevels(consoleTarget);
var fatalTarget = new FileTarget("fatal")
{
FileName = "${basedir}/logs/${shortdate}/fatal-${shortdate}.log",
Layout = "${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}"
};
logConfig.AddTarget(fatalTarget);
logConfig.AddRuleForOneLevel(LogLevel.Fatal, fatalTarget);
var errorTarget = new FileTarget("error")
{
FileName = "${basedir}/logs/${shortdate}/error-${shortdate}.log",
Layout = "${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}"
};
logConfig.AddTarget(errorTarget);
logConfig.AddRuleForOneLevel(LogLevel.Error, errorTarget);
var warningTarget = new FileTarget("warning")
{
FileName = "${basedir}/logs/${shortdate}/warning-${shortdate}.log",
Layout = "${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}"
};
logConfig.AddTarget(warningTarget);
logConfig.AddRuleForOneLevel(LogLevel.Warn, warningTarget);
var infoTarget = new FileTarget("info")
{
FileName = "${basedir}/logs/${shortdate}/info-${shortdate}.log",
Layout = "${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}"
};
logConfig.AddTarget(infoTarget);
logConfig.AddRuleForOneLevel(LogLevel.Info, infoTarget);
var debugTarget = new FileTarget("debug")
{
FileName = "${basedir}/logs/${shortdate}/debug-${shortdate}.log",
Layout = "${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}"
};
logConfig.AddTarget(debugTarget);
logConfig.AddRuleForOneLevel(LogLevel.Debug, debugTarget);
var traceTarget = new FileTarget("trace")
{
FileName = "${basedir}/logs/${shortdate}/trace-${shortdate}.log",
Layout = "${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}"
};
logConfig.AddTarget(traceTarget);
logConfig.AddRuleForOneLevel(LogLevel.Trace, traceTarget);
var allTarget = new FileTarget("all")
{
FileName = "${basedir}/logs/${shortdate}/all-${shortdate}.log",
Layout = "${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}"
};
logConfig.AddTarget(allTarget);
logConfig.AddRuleForAllLevels(allTarget);
return logConfig;
}
my main function:
public static void Main(string[] args)
{
// NLog: setup the logger first to catch all errors
var logConfig = NlogConfig.GetLoggingConfiguration();
var logger = NLog.Web.NLogBuilder.ConfigureNLog(logConfig).GetCurrentClassLogger();
try
{
logger.Debug("init main");
logger.Trace("startup - trace logging test");
logger.Info("startup - info logging test");
logger.Warn("startup - warn logging test");
logger.Error("startup - error logging test");
logger.Fatal("startup - fatal logging test");
CreateWebHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
logger.Fatal(ex, "Stopped program because of exception");
throw;
}
finally
{
// Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
NLog.LogManager.Shutdown();
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
}).UseNLog(); // NLog: setup NLog for Dependency injection
}
and a sample controller:
[ApiController]
[Route("/api/app/parts")]
public class AppPartsController : ControllerBase
{
private readonly ILogger<AppPartsController> _logger;
private IPartsRepository _partsRepo;
public AppPartsController(ILogger<AppPartsController> logger, IPartsRepository repo)
{
_logger = logger;
_partsRepo = repo;
}
[HttpGet]
public async Task<ActionResult> Get()
{
_logger.LogInformation("/api/app/parts has been reached");
try
{
var partsDb = await _partsRepo.GetAllParts();
var parts = Mapper.Map<IEnumerable<AppPartDto>>(partsDb);
return Ok(parts);
}
catch (Exception ex)
{
_logger.LogError(ex, "error in AppPartsController, get all parts ");
return StatusCode(500);
}
}
}
no matter what i do, all the log entries in the 'Main' are written, but the controller entries are not.
afaik, NLog is configured as per their instructions, but obviously I'm missing something here...
Thanks,
Nir
well, there has been a confusion of configs.
when I've looked deeper in the debug folder, I suddenly saw that log files are generated in a place where they shouldn't, with a different naming convention that was in my NLog code config.
this led me on a wide file hunt for a rouge config file - which was subsequently found lurking in the project folder, after being removed from the project, but not deleted from the file system.
I then continued to search for the usages for said config file in my code and found them in all of my repositories.
once the repositories had been converted to use the proper code config, all was fixed, and peace & order had been restored.
I've also updated the logging section in the appsettings.json to look like this (but I'm not really sure if it had any effect):
"Logging": {
"LogLevel": {
"Default": "Trace",
"Microsoft": "Trace"
}
}
I have two Swagger docs generated by Swashbuckle, namely docs/v1 and docs/v2. However docs/v2 doesn't provide information on the action GetV2(). Please help if Swashbuckle has an option to address this.
1.Since the route template appears to be same for actions get() and getv2(), docs v2 doesn't show any info about getV2().
2. Swagger definition doesn't look v1.0/get while appears as v{version}/get in docs/v1
Note:I have refered on apiversioning samples, but not sure what am I missing. All samples refer to Swashbuckle.core while i use Swashbuckle.
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class HelloController : ApiControllerBase
{
[MapToApiVersion("1.0")]
[Route("v{version:apiVersion}/get")]
[HttpGet]
public ProjectSightActionResult Get()
{
return new Ok("Version 1.0");
}
[MapToApiVersion("2.0")]
[Route("v{version:apiVersion}/get")]
[HttpGet]
public ProjectSightActionResult GetV2()
{
return new Ok("Version 2.0");
}
}
This is my controller including two actions, one for version v1 and one for v2. Below is the webapi.config for the route constraint:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Version Start
// https://github.com/Microsoft/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path
// added to the web api configuration in the application setup
var constraintResolver = new DefaultInlineConstraintResolver()
{
ConstraintMap = {["apiVersion"] = typeof( ApiVersionRouteConstraint )}
};
config.MapHttpAttributeRoutes(constraintResolver);
config.AddApiVersioning();
// Version End
// Web API routes
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Filters.Add(new AuthenticationFilter());
// This causes Web API to remove the IPrincipal from any request that enters the Web API pipeline. Effectively, it "un-authenticates" the request.
// https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/authentication-filters
config.SuppressHostPrincipal();
}
My Swagger config has the code:
[assembly: PreApplicationStartMethod(typeof(SwaggerConfig), "Register")]
namespace sample.WebAPI
{
public class SwaggerConfig
{
public static void Register()
{
var thisAssembly = typeof(SwaggerConfig).Assembly;
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.MultipleApiVersions(
(apiDesc, targetApiVersion) => ResolveVersionSupportByRouteConstraint(apiDesc, targetApiVersion),
vc =>
{
vc.Version("v1", "sample.WebAPI");
vc.Version("v2", "sample.WebAPI");
});
}
)
.EnableSwaggerUi(c =>
{
c.EnableDiscoveryUrlSelector();
// If your API supports ApiKey, you can override the default values.
// "apiKeyIn" can either be "query" or "header"
c.EnableApiKeySupport("x-jwt-assertion", "header");
});
}
private static string GetXmlCommentsPath()
{
return string.Format(#"{0}\bin\XmlComments.xml", AppDomain.CurrentDomain.BaseDirectory);
}
private static bool ResolveVersionSupportByRouteConstraint(ApiDescription apiDesc, string targetApiVersion)
{
//check for deprecated versions
var controllerVersionAttributes = apiDesc.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<ApiVersionAttribute>(true);
if (!controllerVersionAttributes.Any())
{
return true; // include when no attributes are defined
}
if (targetApiVersion.StartsWith("v"))
{
targetApiVersion = targetApiVersion.Substring(1); // remove the leading "v" in `v{x.x}`
}
var apiVersion = ApiVersion.Parse(targetApiVersion);
var controllerApiVersion = controllerVersionAttributes
.Where(x => x.Versions.Contains(apiVersion))
.FirstOrDefault();
// has a compatible version, now check the action for [MapToApiVersion]
if (controllerApiVersion != null)
{
var actionMapToAttributes = apiDesc.ActionDescriptor.GetCustomAttributes<MapToApiVersionAttribute>(false);
if (!actionMapToAttributes.Any())
{
return true; // no MapTo attributes matched, then include the action
}
if (actionMapToAttributes.Any(x => x.Versions.Contains(apiVersion)))
{
return true; // include mapped action
}
}
return false;
}
}
}
I'm not sure if you ever solved your problem, but you can now use the official API Explorer for API versioning, which makes Swagger integration simple. You can see a complete working example here.
Here's an abridged version that should work for you:
static void Register( HttpConfiguration configuration )
{
var constraintResolver = new DefaultInlineConstraintResolver() { ConstraintMap = { ["apiVersion"] = typeof( ApiVersionRouteConstraint ) } };
configuration.AddApiVersioning();
configuration.MapHttpAttributeRoutes( constraintResolver );
// note: this option is only necessary when versioning by url segment.
// the SubstitutionFormat property can be used to control the format of the API version
var apiExplorer = configuration.AddVersionedApiExplorer( options => options.SubstituteApiVersionInUrl = true );
configuration.EnableSwagger(
"{apiVersion}/swagger",
swagger =>
{
// build a swagger document and endpoint for each discovered API version
swagger.MultipleApiVersions(
( apiDescription, version ) => apiDescription.GetGroupName() == version,
info =>
{
foreach ( var group in apiExplorer.ApiDescriptions )
{
var description = "A sample application with Swagger, Swashbuckle, and API versioning.";
if ( group.IsDeprecated )
{
description += " This API version has been deprecated.";
}
info.Version( group.Name, $"Sample API {group.ApiVersion}" )
.Contact( c => c.Name( "Bill Mei" ).Email( "bill.mei#somewhere.com" ) )
.Description( description )
.License( l => l.Name( "MIT" ).Url( "https://opensource.org/licenses/MIT" ) )
.TermsOfService( "Shareware" );
}
} );
swagger.IncludeXmlComments( XmlCommentsFilePath );
} )
.EnableSwaggerUi( swagger => swagger.EnableDiscoveryUrlSelector() );
}
}
static string XmlCommentsFilePath
{
get
{
var basePath = System.AppDomain.CurrentDomain.RelativeSearchPath;
var fileName = typeof( Startup ).GetTypeInfo().Assembly.GetName().Name + ".xml";
return Path.Combine( basePath, fileName );
}
}
I have the following layout for my mvc project:
/Controllers
/Demo
/Demo/DemoArea1Controller
/Demo/DemoArea2Controller
etc...
/Views
/Demo
/Demo/DemoArea1/Index.aspx
/Demo/DemoArea2/Index.aspx
However, when I have this for DemoArea1Controller:
public class DemoArea1Controller : Controller
{
public ActionResult Index()
{
return View();
}
}
I get the "The view 'index' or its master could not be found" error, with the usual search locations.
How can I specify that controllers in the "Demo" namespace search in the "Demo" view subfolder?
You can easily extend the WebFormViewEngine to specify all the locations you want to look in:
public class CustomViewEngine : WebFormViewEngine
{
public CustomViewEngine()
{
var viewLocations = new[] {
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx",
"~/AnotherPath/Views/{0}.ascx"
// etc
};
this.PartialViewLocationFormats = viewLocations;
this.ViewLocationFormats = viewLocations;
}
}
Make sure you remember to register the view engine by modifying the Application_Start method in your Global.asax.cs
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomViewEngine());
}
Now in MVC 6 you can implement IViewLocationExpander interface without messing around with view engines:
public class MyViewLocationExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context) {}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
return new[]
{
"/AnotherPath/Views/{1}/{0}.cshtml",
"/AnotherPath/Views/Shared/{0}.cshtml"
}; // add `.Union(viewLocations)` to add default locations
}
}
where {0} is target view name, {1} - controller name and {2} - area name.
You can return your own list of locations, merge it with default viewLocations (.Union(viewLocations)) or just change them (viewLocations.Select(path => "/AnotherPath" + path)).
To register your custom view location expander in MVC, add next lines to ConfigureServices method in Startup.cs file:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new MyViewLocationExpander());
});
}
There's actually a lot easier method than hardcoding the paths into your constructor. Below is an example of extending the Razor engine to add new paths. One thing I'm not entirely sure about is whether the paths you add here will be cached:
public class ExtendedRazorViewEngine : RazorViewEngine
{
public void AddViewLocationFormat(string paths)
{
List<string> existingPaths = new List<string>(ViewLocationFormats);
existingPaths.Add(paths);
ViewLocationFormats = existingPaths.ToArray();
}
public void AddPartialViewLocationFormat(string paths)
{
List<string> existingPaths = new List<string>(PartialViewLocationFormats);
existingPaths.Add(paths);
PartialViewLocationFormats = existingPaths.ToArray();
}
}
And your Global.asax.cs
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ExtendedRazorViewEngine engine = new ExtendedRazorViewEngine();
engine.AddViewLocationFormat("~/MyThemes/{1}/{0}.cshtml");
engine.AddViewLocationFormat("~/MyThemes/{1}/{0}.vbhtml");
// Add a shared location too, as the lines above are controller specific
engine.AddPartialViewLocationFormat("~/MyThemes/{0}.cshtml");
engine.AddPartialViewLocationFormat("~/MyThemes/{0}.vbhtml");
ViewEngines.Engines.Add(engine);
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
One thing to note: your custom location will need the ViewStart.cshtml file in its root.
If you want just add new paths, you can add to the default view engines and spare some lines of code:
ViewEngines.Engines.Clear();
var razorEngine = new RazorViewEngine();
razorEngine.MasterLocationFormats = razorEngine.MasterLocationFormats
.Concat(new[] {
"~/custom/path/{0}.cshtml"
}).ToArray();
razorEngine.PartialViewLocationFormats = razorEngine.PartialViewLocationFormats
.Concat(new[] {
"~/custom/path/{1}/{0}.cshtml", // {1} = controller name
"~/custom/path/Shared/{0}.cshtml"
}).ToArray();
ViewEngines.Engines.Add(razorEngine);
The same applies to WebFormEngine
Instead of subclassing the RazorViewEngine, or replacing it outright, you can just alter existing RazorViewEngine's PartialViewLocationFormats property. This code goes in Application_Start:
System.Web.Mvc.RazorViewEngine rve = (RazorViewEngine)ViewEngines.Engines
.Where(e=>e.GetType()==typeof(RazorViewEngine))
.FirstOrDefault();
string[] additionalPartialViewLocations = new[] {
"~/Views/[YourCustomPathHere]"
};
if(rve!=null)
{
rve.PartialViewLocationFormats = rve.PartialViewLocationFormats
.Union( additionalPartialViewLocations )
.ToArray();
}
Last I checked, this requires you to build your own ViewEngine. I don't know if they made it easier in RC1 though.
The basic approach I used before the first RC was, in my own ViewEngine, to split the namespace of the controller and look for folders which matched the parts.
EDIT:
Went back and found the code. Here's the general idea.
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
{
string ns = controllerContext.Controller.GetType().Namespace;
string controller = controllerContext.Controller.GetType().Name.Replace("Controller", "");
//try to find the view
string rel = "~/Views/" +
(
ns == baseControllerNamespace ? "" :
ns.Substring(baseControllerNamespace.Length + 1).Replace(".", "/") + "/"
)
+ controller;
string[] pathsToSearch = new string[]{
rel+"/"+viewName+".aspx",
rel+"/"+viewName+".ascx"
};
string viewPath = null;
foreach (var path in pathsToSearch)
{
if (this.VirtualPathProvider.FileExists(path))
{
viewPath = path;
break;
}
}
if (viewPath != null)
{
string masterPath = null;
//try find the master
if (!string.IsNullOrEmpty(masterName))
{
string[] masterPathsToSearch = new string[]{
rel+"/"+masterName+".master",
"~/Views/"+ controller +"/"+ masterName+".master",
"~/Views/Shared/"+ masterName+".master"
};
foreach (var path in masterPathsToSearch)
{
if (this.VirtualPathProvider.FileExists(path))
{
masterPath = path;
break;
}
}
}
if (string.IsNullOrEmpty(masterName) || masterPath != null)
{
return new ViewEngineResult(
this.CreateView(controllerContext, viewPath, masterPath), this);
}
}
//try default implementation
var result = base.FindView(controllerContext, viewName, masterName);
if (result.View == null)
{
//add the location searched
return new ViewEngineResult(pathsToSearch);
}
return result;
}
Try something like this:
private static void RegisterViewEngines(ICollection<IViewEngine> engines)
{
engines.Add(new WebFormViewEngine
{
MasterLocationFormats = new[] {"~/App/Views/Admin/{0}.master"},
PartialViewLocationFormats = new[] {"~/App/Views/Admin//{1}/{0}.ascx"},
ViewLocationFormats = new[] {"~/App/Views/Admin//{1}/{0}.aspx"}
});
}
protected void Application_Start()
{
RegisterViewEngines(ViewEngines.Engines);
}
Note: for ASP.NET MVC 2 they have additional location paths you will need to set for views in 'Areas'.
AreaViewLocationFormats
AreaPartialViewLocationFormats
AreaMasterLocationFormats
Creating a view engine for an Area is described on Phil's blog.
Note: This is for preview release 1 so is subject to change.
Most of the answers here, clear the existing locations by calling ViewEngines.Engines.Clear() and then add them back in again... there is no need to do this.
We can simply add the new locations to the existing ones, as shown below:
// note that the base class is RazorViewEngine, NOT WebFormViewEngine
public class ExpandedViewEngine : RazorViewEngine
{
public ExpandedViewEngine()
{
var customViewSubfolders = new[]
{
// {1} is conroller name, {0} is action name
"~/Areas/AreaName/Views/Subfolder1/{1}/{0}.cshtml",
"~/Areas/AreaName/Views/Subfolder1/Shared/{0}.cshtml"
};
var customPartialViewSubfolders = new[]
{
"~/Areas/MyAreaName/Views/Subfolder1/{1}/Partials/{0}.cshtml",
"~/Areas/MyAreaName/Views/Subfolder1/Shared/Partials/{0}.cshtml"
};
ViewLocationFormats = ViewLocationFormats.Union(customViewSubfolders).ToArray();
PartialViewLocationFormats = PartialViewLocationFormats.Union(customPartialViewSubfolders).ToArray();
// use the following if you want to extend the master locations
// MasterLocationFormats = MasterLocationFormats.Union(new[] { "new master location" }).ToArray();
}
}
Now you can configure your project to use the above RazorViewEngine in Global.asax:
protected void Application_Start()
{
ViewEngines.Engines.Add(new ExpandedViewEngine());
// more configurations
}
See this tutoral for more info.
I did it this way in MVC 5. I didn't want to clear the default locations.
Helper Class:
namespace ConKit.Helpers
{
public static class AppStartHelper
{
public static void AddConKitViewLocations()
{
// get engine
RazorViewEngine engine = ViewEngines.Engines.OfType<RazorViewEngine>().FirstOrDefault();
if (engine == null)
{
return;
}
// extend view locations
engine.ViewLocationFormats =
engine.ViewLocationFormats.Concat(new string[] {
"~/Views/ConKit/{1}/{0}.cshtml",
"~/Views/ConKit/{0}.cshtml"
}).ToArray();
// extend partial view locations
engine.PartialViewLocationFormats =
engine.PartialViewLocationFormats.Concat(new string[] {
"~/Views/ConKit/{0}.cshtml"
}).ToArray();
}
}
}
And then in Application_Start:
// Add ConKit View locations
ConKit.Helpers.AppStartHelper.AddConKitViewLocations();