For lightweight, small Web APIs (microservices 🤧), you may not use MVC at all and instead rely only on ASP.NET Core Routing.
public class Program
{
public static async Task Main(string[] args) =>
await WebHost.CreateDefaultBuilder(args)
.ConfigureServices(s => s.AddRouting())
.Configure(app =>
{
.UseRouter(r => // define all API endpoints
{
r.MapGet("contacts", async (req, res, data) =>
{ });
r.MapGet("contacts/{id:int}", async (req, res, data) =>
{ });
});
})
.Build().RunAsync();
}
You can choose which MVC features you need with services.AddMvcCore() instead of services.AddMvc().
// kitchen sink
services.AddMvc();
// pick what you need
services.AddMvcCore()
.AddDataAnnotations() // for model validation
.AddJsonFormatters() // for JSON
.AddApiExplorer(); // for Swagger
Thanks to DI child scopes, you can have multiple isolated ASP.NET Core pipelines, with their own DI containers, in the same process.
MVC scans the entire AssemblyLoadContext to discover its working parts, so they have to be constrained separately.
public class HiStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IGreetService, HiService>();
}
public void Configure(IApplicationBuilder app)
{
app.Run(async c =>
{
var svc = c.RequestServices.GetRequiredService<IGreetService>();
await c.Response.WriteAsync(svc.Greet());
});
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{ }
public void Configure(IApplicationBuilder app)
{
app.IsolatedMap<HiStartup>("/hi-branch");
app.IsolatedMap<OtherStartup>("/other-branch");
}
}
It is possible to use virtual file systems in MVC apps by implementing an IFileProvider.
Multiple providers can be used together via a CompositeFileProvider.
var blobFileProvider = app.ApplicationServices.GetRequiredService<AzureBlobFileProvider>();
app.UseStaticFiles(new StaticFileOptions()
{
FileProvider = blobFileProvider,
RequestPath = "/files"
}.
UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = blobFileProvider,
RequestPath = "/files"
});
ApplicationPartManager is used to load ASP.NET Core MVC features.
services.AddMvc().ConfigureApplicationPartManager(a =>
{
var binDirectory = fileProvider.GetDirectoryContents("plugins");
foreach (var item in binDirectory)
{
using (var assemblyStream = item.CreateReadStream())
{
using (MemoryStream ms = new MemoryStream())
{
assemblyStream.CopyTo(ms);
var assembly = Assembly.Load(ms.ToArray());
a.ApplicationParts.Add(new AssemblyPart(assembly));
}
}
}
});
IActionDescriptorChangeProvider can be used to rebuild the MVC action descriptor cache.
This allows building plugin systems, where controllers and actions are added and removed dynamically, without restarting the application
var assembly = Assembly.Load(assemblyPath);
var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly);
foreach (var part in partFactory.GetApplicationParts(assembly))
{
applicationPartManager.ApplicationParts.Add(part);
}
actionDescriptorChangeProvider.TokenSource.Cancel();
With Microsoft.NET.Sdk.Razor it is possible to precompile Razor views and distribute them in a DLL.
MVC ships with several built-in IApplicationModelProviders to define its own behaviors.
[ApiController]
[Route("[controller]")]
public class BookController : Controller
{
[HttpPost]
public IActionResult PostBook([FromBody]Book book)
{
if (ModelState.IsValid)
{
return BadRequest(ModelState);
}
// controller logic
}
}
IApplicationModelProvider should be used by frameworks and libraries, while
IApplicationModelConvention by applications.
Providers always run before conventions.
public class GlobalRoutePrefixConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
var route = new RouteAttribute("api/[controller]");
var prefix = new AttributeRouteModel(route);
foreach (var controller in application.Controllers)
{
foreach (var selector in controller.Selectors)
{
selector.AttributeRouteModel =
selector.AttributeRouteModel != null
? AttributeRouteModel.CombineAttributeRouteModel(prefix, selector.AttributeRouteModel)
: prefix;
}
}
}
}
IActionConstraint is used to impose additional action matching rules.
IActionConstraint may disambiguate multiple actions from matching the same request or reject a certain request from being handled by an action candidate.
public class SwissAcceptLanguageActionConstraint : IActionConstraint, IActionConstraintMetadata
{
public int Order => 0;
public bool Accept(ActionConstraintContext ctx)
{
var headers = ctx.RouteContext.HttpContext.Request.GetTypedHeaders();
return headers.AcceptLanguage != null &&
headers.AcceptLanguage.Any(x => x.Value.Equals("de-CH", StringComparison.OrdinalIgnoreCase)))
}
}