Matthew Groves
Matthew Groves

Reputation: 26096

Adding asp.net controllers/views from a Class Library

I am building a class library in C# with .NET 6 (possibly 7) for use with ASP.NET Core sites.

I would like this class library to (among other things), contain a configuration UI (kinda like how Swashbuckle builds up an OpenAPI UI - I checked out the source code and couldn't quite wrap my mind around that portion).

I figured creating controllers/views in the class library would be the way to go.

The controllers are working automatically (although, I'm mildly concerned about routing conflicts -- what if I have a /foo/bar route in my class library and the project using this library also has a /foo/bar route?).

The views, however, do not seem to be added automatically. I've tried embedding and following the folder conventions, but I still get the error:

An unhandled exception occurred while processing the request. InvalidOperationException: The view 'Index' was not found. The following locations were searched: /Views/Foo/Index.cshtml /Views/Shared/Index.cshtml

Here's the relevant portion of the class library:

class library structure

What do I have to do to get these Views loaded/parsed/working from a class library? (Or is there a better alternative to doing what I'm trying to do without Views?)

Upvotes: 2

Views: 2154

Answers (2)

Matthew Groves
Matthew Groves

Reputation: 26096

ApplicationParts is the answer, but I found the documentation a little hard to follow, so here's what I did:

var assembly = typeof(FooController).Assembly;

@this.AddControllersWithViews()
    .AddApplicationPart(assembly)
    .AddRazorRuntimeCompilation();      

@this.Configure<MvcRazorRuntimeCompilationOptions>(options => 
    { options.FileProviders.Add(new EmbeddedFileProvider(assembly)); });   

The key for me was .AddRazorRuntimeCompilation(); and the following line to add a file provider.

Upvotes: 1

iCollect.it Ltd
iCollect.it Ltd

Reputation: 93551

I followed the previous answer and found it, and the referenced pages, were missing key details to make it work (marking views as embedded resources and views appear to become case sensitive):

Research steps:

  1. Cloned the Microsoft Sample app mentioned on this page

  2. Ran WebAppParts sample (.Net core 3.0) - Worked

  3. Updated WebAppParts sample to .Net 6.0 - Worked

  4. Created a brand new .Net 6 Web Application

  5. Added MySharedApp .Net 6 library

  6. Added Snippet to the program main setup and called this instead of AddControllersWithViews

    public static void ConfigureServices(IServiceCollection services)
    {
        var assembly = typeof(MySharedController).Assembly;
        services.AddControllersWithViews()
            .AddApplicationPart(assembly)
            .AddRazorRuntimeCompilation();
    
        services.Configure<MvcRazorRuntimeCompilationOptions>(options =>
        { options.FileProviders.Add(new EmbeddedFileProvider(assembly)); });
    }
    
  7. Added menu hyperlink to MyShared/index in _Layout.cshtml - It cannot find the view

  8. Diff'ed the sample project against my new one and slowly migrated settings/changes across.

  9. Added Microsoft.Extensions.FileProviders.Embedded NuGet to library.

  10. Found that any shared views needed to be marked as Embedded resource in the views Build Action. This was not apparent in the sample app as the view displays as Content in the sample app, despite this entry in the .csproj

<ItemGroup>
    <EmbeddedResource Include="Views\MyShared\Index.cshtml" />
</ItemGroup>
  1. After all these changes, my .Net 6.0 test web app started sharing the library's views and controller.

  2. Final example below is what I reduced my app code to in Program.Main(). Feel free to remove the Auth or Json lines if not needed:

       var assembly = typeof(AnySharedController).Assembly;
    
       builder.Services.AddRazorPages()
           .AddJsonOptions(x => x.JsonSerializerOptions.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)
           .AddApplicationPart(assembly)
           .AddRazorRuntimeCompilation()
           .AddMicrosoftIdentityUI();
    
       builder.Services.Configure<MvcRazorRuntimeCompilationOptions>(options =>
       {
           options.FileProviders.Add(new EmbeddedFileProvider(assembly));
       });
    
  3. While migrating Views to a library, I found that partial views were not found unless the view or partial view name was the same case.

e.g. This one failed

@(await Html.PartialAsync("titlepartial"))

Then this one worked

@(await Html.PartialAsync("TitlePartial"))

Upvotes: 1

Related Questions