For lightweight, small Web APIs (microservices 🤧), you may not use MVC at all and instead rely only on ASP.NET Core Routing.
public static async Task Main(string[] args) =>
await WebHost.CreateDefaultBuilder(args)
.ConfigureServices(s => s.AddRouting())
.Configure(app =>
{
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();
public static async Task Main(string[] args) =>
await WebHost.CreateDefaultBuilder(args)
.ConfigureServices(s => s.AddRouting())
.Configure(app =>
{
app.UseRouting(r =>
{
r.MapGet("/contacts", async context =>
{ });
r.MapGet("contacts/{id:int}", async context =>
{ }).RequireAuthorization("API");
});
}).Build().RunAsync();
[HttpGet("contacts")]
public IActionResult Get()
{
var contacts = new[]
{
new Contact { Name = "Filip", City = "Zurich" },
new Contact { Name = "Not Filip", City = "Not Zurich" }
};
// will do content negotation
return new ObjectResult(contacts);
}
ASP.NET Core 2.1 introduced IActionResultExecutor<T> which allows us to use IActionResults outside of controllers.
r.MapGet("contacts", async (request, response, routeData) =>
{
var contacts = new[]
{
new Contact { Name = "Filip", City = "Zurich" },
new Contact { Name = "Not Filip", City = "Not Zurich" }
};
var result = new ObjectResult(contacts);
await response.WriteActionResult(result);
});
You can run multiple isolated ASP.NET Core pipelines, with their own DI containers, in the same process (parent application).
public class CustomerStartup
{
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<CustomerStartup>("/customer");
app.IsolatedMap<AdminStartup>("/admin");
}
}
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"
};
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();
.NET Core 3.0 adds support for collectible assemblies, which is perfect for plugins.
With Microsoft.NET.Sdk.Razor it is possible to precompile Razor views and distribute them in a DLL.
At application startup the default MVC behaviors can be changed by modifying the application model.
[ApiController]
[Route("[controller]")]
public class BookController : Controller
{
[HttpPost]
public IActionResult PostBook([FromBody]Book book)
{
if (ModelState.IsValid)
{
return BadRequest(ModelState);
}
// controller logic
}
}
public class AuthorizeConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
foreach (var actionModel in controller.Actions)
{
if (actionModel.ActionName.StartsWith("Authorized") &&
!actionModel.Filters.OfType<IAuthorizeData>().Any())
{
actionModel.Filters.Add(new AuthorizeFilter());
}
}
}
}
}
IApplicationModelProvider should be used by frameworks and libraries, while
IApplicationModelConvention by applications.
Providers always run before conventions.
IActionConstraint is used to impose additional action matching rules.
public class OrderController
{
[Route("order")]
public IActionResult Process(Order order)
{
// handle VIP customers
// handle standard customers
// handle basic customers
}
}
public class OrderController
{
[VIPOnly]
[Route("order")]
public IActionResult ProcessVIP(Order order) { }
[StandardOnly]
[Route("order")]
public IActionResult ProcessStandard(Order order) { }
[BasicOnly]
[Route("order")]
public IActionResult ProcessBasic(Order order) { }
}
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)))
}
}