adapting
ASP.NET Core MVC
to your needs

Filip W

a random guy from the internet

strathweb.com @filip_woj github.com/filipw

Setup

Setup

  • 🕊 No MVC
    ASP.NET Core apps built on IRouter
  • 🎯 AddMvcCore() vs AddMvc()
    Use the things you need
  • 👯 MVC side-by-side
    Multiple sub-applications in the same app


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().

Pick what you need

    
// 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");
    }
}

Plugins and Extensibility

Plugins and Extensibility

  • 📋 IFileProvider
    Virtual file system
  • 📦 ApplicationPartManager
    Externally loaded features
  • 🏗 Change providers
    Rebuild application model at runtime
  • 🎁 Packaging Razor
    Ship MVC plugins with their own embedded UI

It is possible to use virtual file systems in MVC apps by implementing an IFileProvider.

Multiple providers can be used together via a CompositeFileProvider.

Custom file provider

    
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.

Adding controllers from external assembly

    
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

Adding an assembly to a running MVC app

    
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 Application Model

Application Model

  • 🔧 IApplicationModelProvider
    Compose the application model
  • 📚 IApplicationModelConvention
    Customize the application model
  • ⛔️ IActionConstraint
    Customize the action selection


Application Model

  • ✅ Controllers
    Defined behaviours
  • ✅ Actions & Parameters
    Select relevant methods, defined behaviours
  • ✅ Filters
    Apply filters
  • ✅ Selectors
    Mapping to routing

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)))
    }
}
    

Thank you