How to load controllers from different versions of the same assembly - c#

I am trying to solve the following problem and i am not exactly sure how to do it:
I am building a web server that has differenty APIs/Controllers that are loaded from .dll-Files on Startup. It will run in a linux docker container and is implemented as an ASP-NET Webapplication & .net Core 2.1 .
The loading of assemblies that contain controllers works fine by doing something like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddMvc().AddApplicationPart(AssemblyLoadContext.Default.LoadFromAssemblyPath("/PATH/APIPlugin.dll"));
}
This application must have versioned REST-APIs that means: I need to load the same assembly multiple times in different versions. then i need to have some kind of routing between the versions.
For example:
/api/data/latest/
/api/data/v1/
I cannot use AssemblyLoadContext.Default.LoadFromAssemblyPath to load multiple versions of the same assembly. I also tried to grab the controller from the assembly and creating an instance of it like this:
var controllerAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath("/PATH/APIPlugin.dll");
var pluginType = controllerAssembly ExportedTypes.First<Type>();
var pluginInstance = (ControllerBase)Activator.CreateInstance(pluginType);
services.Add(new ServiceDescriptor(pluginType, pluginInstance));
This throws no exception but ultimately does not work. But i am pretty new to ASP.Net so this might very well be nonsense and i would have to find a solutuion to route between the different versions, even if it would work like this.
My Question:
How would one approach this requirement ?
Is it a god idea/possible to load multiple Controllers from the "same" assembly ? If yes, how would one achieve this?
Or would it be a better solution to have one controller that does all the routuing and load some self-defined implementation from the assemblies. So that the controller would route between the versions, and api-methods?

I was able to find a sultion while tinkering around:
public class ControllerPluginProvider : IApplicationFeatureProvider<ControllerFeature>
{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
var basePath = AppContext.BaseDirectory;
var pluginPath = Path.Combine(basePath, "plugins");
foreach (var file in Directory.GetFiles(pluginPath, "*.dll")){
var assembly = Assembly.LoadFile(file);
var controllers = assembly.GetExportedTypes().Where(t => typeof(ControllerBase).IsAssignableFrom(t));
foreach (var candidate in controllers)
{
feature.Controllers.Add(candidate.GetTypeInfo());
}
}
}
}
In Startup:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMvc().ConfigureApplicationPartManager(m =>
m.FeatureProviders.Add(new ControllerPluginProvider()));
}
This lead to the following error when the same assembly, and therefore a Controller with the same name was loaded: Attribute routes with the same name 'Get' must have the same template
I was able to fix it, and also add versioning with the versioning library:
https://github.com/microsoft/aspnet-api-versioning
[ApiVersion("2.0")]
[ApiController]
[Route("api/v{version:apiVersion}/MyController")]
public class MyControllerController : ControllerBase
{
}
Now the only step that is missing, is routing the /latest/ path to the most recent controller of the given type. I have not found a solution to this yet, but this should be doable.

Related

calling AddAutoMapper once per assembly instead of passing in multiple assemblies?

I have a multi layered project with a web API project and a library project. Both projects rely on AutoMapper (and AutoMapper extensions for Microsoft.Extensions.DependencyInjection). Based on this
https://docs.automapper.org/en/latest/Dependency-injection.html#asp-net-core
in the Startup file I'm setting up AutoMapper for all the layers
Assembly apiAssembly = Assembly.GetExecutingAssembly();
Assembly myLibraryAssembly = Assembly.Load("MyLibrary");
services.AddAutoMapper(apiAssembly, myLibraryAssembly);
As you can see here, the API project needs to know about all the referenced library projects by loading them via name. I would prefer a way that every project is able to register itself. Based on this sample code
https://github.com/jasontaylordev/CleanArchitecture/blob/master/src/Application/DependencyInjection.cs
I created such a file in my library project
public static class DependencyInjection
{
public static IServiceCollection AddMyLibrary(this IServiceCollection services)
{
Assembly executingAssembly = Assembly.GetExecutingAssembly(); // MyLibrary assembly
services.AddAutoMapper(executingAssembly);
// ... setup other services
return services;
}
}
and in the API project I can now do this
Assembly executingAssembly = Assembly.GetExecutingAssembly();
services.AddAutoMapper(executingAssembly);
services.AddMyLibrary();
The code seems to work fine but AddAutoMapper will be called twice. Once for the API assembly and once for the library assembly. Should I stick to the first approach because AutoMapper should only be added once or is it fine to separate it?
The accepted answer was correct at the time but it would appear that things have changed in the recent past.
The AutoMapper.Extensions.Microsoft.DependencyInjection package has been updated to allow the call to AddAutoMapper() multiple times.
See PR Use Microsoft.Extensions.Options to configure AutoMapper for details. You will need to update the package to version 8.0.0 or higher to use it.
The code seems to work fine but AddAutoMapper will be called twice. Once for the API assembly and once for the library assembly. Should I stick to the first approach because AutoMapper should only be added once or is it fine to separate it?
You should stick to the first approach, because AddAutoMappper does nothing when called for the second, third etc. time, thus profiles and other AM related types from the assemblies passed to these calls won't be registered.
It can be seen in the beginning of the implementation of the private method which is called by all public AddAutoMapper overloads:
private static IServiceCollection AddAutoMapperClasses(IServiceCollection services, Action<IServiceProvider, IMapperConfigurationExpression> configAction,
IEnumerable<Assembly> assembliesToScan, ServiceLifetime serviceLifetime = ServiceLifetime.Transient)
{
// Just return if we've already added AutoMapper to avoid double-registration
if (services.Any(sd => sd.ServiceType == typeof(IMapper)))
return services;
followed by the actual registration code, which at the end registers IMapper.
Currently there is an open issue Allow usage of Options Pattern to configure AutoMapper #132 with exactly the same concerns as yours.
You can use abp moudule system to make the library register for themselves.
The doc is here: https://docs.abp.io/en/abp/latest/Module-Development-Basics
But the framework is a little heavy.If you don't want to use it,
I think you can do it this way: every library register the automapper itself ,and call the other library's register function it depends on.
Each library can add a helper class to do the registration
public static class ApiRegisterHelper
{
public static Assembly GetAssembly()
{
return Assembly.GetExecutingAssembly();
}
public static IServiceCollection AddLibrary(IServiceCollection services)
{
Assembly executingAssembly = GetAssembly();
services.AddAutoMapper(executingAssembly);
DaoRegisterHelper.AddLibrary(services);
return services;
}
}
public static class DaoRegisterHelper
{
public static Assembly GetAssembly()
{
return Assembly.GetExecutingAssembly();
}
public static IServiceCollection AddLibrary(IServiceCollection services)
{
Assembly executingAssembly = GetAssembly();
services.AddAutoMapper(executingAssembly);
OtherRegisterHelper.AddLibrary();
return services;
}
}
I havn't tested it,but it might be work.Hope it helps.

AspNet Core AttributeRouting does not discover routes if Startup is in different assembly

I have a scenario where we have a "standardised startup" for many small AspNet Core websites.
A seemingly obvious solution to achieve this is to refactor the Startup.cs class into a separate common assembly (as Infrastructure.Web.Core.Startup). We then have each small AspNet Core website reference it the common assembly and use that startup class instead:
public static Microsoft.AspNetCore.Hosting.IWebHostBuilder CreateWebHostBuilder( string[] args )
{
return new WebHostBuilder()
.UseKestrel()
.ConfigureServices( collection => { } )
.UseContentRoot( System.IO.Directory.GetCurrentDirectory() )
.UseStartup<Infrastructure.Web.Core.Startup>(); //.UseStartup<Startup>();
}
Somehow, this breaks attribute routing in the sense that the routes are not hit. No errors, but not routing. The moment I copy the class back into the website project (with the exact same code) it works again.
As a test, if I wrap the Startup.cs class in the common library in a local startup class (like below), it also works:
public class Startup
{
private readonly Infrastructure.Web.Core.Startup _startup;
public Startup( IConfiguration configuration )
{
_startup = new Infrastructure.Web.Core.Startup( configuration );
}
public IConfiguration Configuration => _startup.Configuration;
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices( IServiceCollection services )
{
_startup.ConfigureServices( services );
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure( IApplicationBuilder app, IHostingEnvironment env )
{
_startup.Configure( app, env );
}
}
If I had to take a guess, it's probably something to do with the Dependency Injection.
Does anyone have any suggestions?
FYI: It's using typical AspNet Core 2.1 projects
UPDATE
Incidentally, if I use inheritance it also works but the derived class must be in the same project as the website. I guess it seems obvious, but thought I include that information for completeness sake:
public class Startup : Infrastructure.Web.Core.Startup
{
public Startup( IConfiguration configuration ) : base(configuration)
{
}
}
You can fix this by adding the following statement to your services in your Startup.cs method.
services.AddApplicationPart(typeof(AnTypeInTheOtherAssembly).Assembly);
This will tell the View/Controller Discovery to also check for the new location. Your Project which contains the Startup.cs file would be the Startup Project, and all the others would be just references and libraries or similar.
As of .Net Core 3 you can use something called Razor Class Libraries, see the MSDN. This will automatically add this your Controllers and Views to the discovery, it also has debugging support and will work just as a normal Class Library would.

Webapi 2 controllers on 2 projects

I have a webapi2 C# projects on which I have all the controllers in a controllers folder.
I now have some functionality that I want to add but I want to put it on a different visual studio project that will have its own controllers (and models and views) folder. Is this possible so that I can create some kind of module that will be loaded by webapi2?
Web Api relies on the IHttpControllerSelector for choosing the api controller for handling a request, of which has a default implementation which relies on IAssembliesResolver for resolving the assemblies to search for the api controllers.
In the least minimum change, you can replace this assembly resolver with a custom implementation which will load other libraries for you.
A very naive example might look like the following:
public class CustomAssemblyResolver : IAssembliesResolver
{
public List<string> PluginNames { get; set; }
public CustomAssemblyResolver()
{
PluginNames = new List<string>();
//Add the custom libraries here
PluginNames.Add("Your_Second_Library");
}
public ICollection<Assembly> GetAssemblies()
{
var asms = AppDomain.CurrentDomain.GetAssemblies().ToList();
foreach (var name in PluginNames)
{
var asmPath = System.IO.Path.Combine(HostingEnvironment.MapPath("~/"), name + ".dll");
try
{
var asm= System.Reflection.Assembly.LoadFrom(asmPath);
if(!asms.Contains(asm))
asms.Add(asm);
}
catch (Exception)
{
}
}
return asms;
}
}
You can then replace the default resolver with this code
config.Services.Replace(typeof(IAssembliesResolver), new CustomAssemblyResolver());
inside your Register method of WebApiConfig class.
Then, copy all your additional libraries with controller classes to the bin directory and you are done.
If you need even further customization for controller selection, you can go for custom implementation of IHttpControllerSelector and replace the existing implementation in a similar fashion.
You can create the functionality in a library project, and then reference that project to the your webapi2 and your other visual studio project. Basically you will have three solutions; two webapi solutions and one library solution. The library solution will contain the required logic needed for the two webapi solutions.
You shouldn't have a need for controllers in two projects.
If the controllers are for COMPLETELY different business domains, make two api's in IIS w/ two solutions.
If they are similar, create all of your controllers in the web project. Then have these controllers call out to separate application services.
public CustomerAccountsController : ApiController
{
private CustomerAccountService _customerAccountService; // lives in application layer project
public CustomerAccountsController()
{
// di stuff
}
public HttpResponseMessage PutCancelAccount( int accountId )
{
// exception handling + logging
_customerAccountService.CancelAccount(accountId);
// return status code if success, or if an exception
}
}
public OrderController : ApiController
{
private OrderService _orderService; // lives in application layer project
public OrderController()
{ //di stuff
}
public HttpResponseMessage PostCreateOrder(CreateOrderRequest createOrderRequest)
{
// exception handling + logging
_orderService.TakeOrder(createOrderRequest);
// return status code if success, or if an exception
}
}
So, most of your logic should hide behind application layer services, and these services should have methods that map 1-1 to use cases. If your business domain for these two applications are completely different, just create two separate solutions and two separate IIS applications/api's
No, it is not possible. The maximum you can do is create a class library that will compile as a DLL, then reference that DLL in your WebApi. Otherwise, you will be obliged to either put everything in the same application (WebApi) or create two different WebApi applications.
Depending on your needs...
My advise, just put the 2 controllers on a single project and create a helper/service folder/class in your other project and call those services whenever you need.
This is not really the answer to your question but I believe this will help. Normally we create a solution using this folder structure, I hope this helps:
MyTeamSolution
- MyTeam.Core = Class libraries
> Models
> Model1.cs
> Model2.cs
> Model3.cs
> Interface
> ISomeInterface.cs
> Helpers
> HelperClass.cs
- MyTeam.Api = webapi project
> Model1Controller.cs
> Model2Controller.cs
> Model3Controller.cs
- MyTeam.Web = mvc project
> Controllers
> Models
> Views
> etc.
- MyTeam.Sql = Class libraries
> MyTeamDbContext.cs

Owin/Katana System.EntryPointNotFoundException Assembly located elsewhere

Somehow I can't find someone having the same problem.
We have a plugin based project, on the main folder we have the Plugin Starter, the Bootstrapper, and some dependencies.
The plugins are in the "Plugins" folder and within, are some other folders.
My Startup.cs file is as following:
public class Startup
{
public void Configuration(IAppBuilder appBuilder)
{
appBuilder.Use(async (env, next) =>
{
new object().Info(string.Concat("Http method: ", env.Request.Method, ", path: ", env.Request.Path));
await next();
new object().Info(string.Concat("Response code: ", env.Response.StatusCode));
});
RunWebApiConfiguration(appBuilder);
}
private static void RunWebApiConfiguration(IAppBuilder appBuilder)
{
var httpConfiguration = new HttpConfiguration();
httpConfiguration.Routes.MapHttpRoute(
name: "WebApi"
, routeTemplate: "{controller}/{id}"
, defaults: new { id = RouteParameter.Optional }
);
appBuilder.UseWebApi(httpConfiguration);
}
}
The call is made as following:
WebApp.Start<Startup>("http://localhost/MyRestApi");
if I load the Assembly on the same folder, no problem, but if I load it where it "belongs", I can't get Owin to find it.
Anyone had ever ran into this or have any idea? I might think of something like a configuration line in the App.config, but I don't find this as a solution.
UPDATE 1:
I get the System working again when I copy the Rest Service assembly on the main directory but then it is loaded two times, which is a big problem.
When I send a Rest request, I get the following message:
{"Message":"An error has occurred.","ExceptionMessage":"Multiple types were found that match the controller named 'ExternalOrder'. This can happen if the route that services this request ('{controller}/{id}') found multiple controllers defined with the same name but differing namespaces, which is not supported.\r\n\r\nThe request for 'ExternalOrder' has found the following matching controllers:\r\nInternalOrderValidationPlugin.Controllers.ExternalOrderController\r\nInternalOrderValidationPlugin.Controllers.ExternalOrderController","ExceptionType":"System.InvalidOperationException","StackTrace":" at System.Web.Http.Dispatcher.DefaultHttpControllerSelector.SelectController(HttpRequestMessage request)\r\n at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()"}
Ok, after wandering around on the internet, I found a solution, and namely Dependency Injection.
Here in this post "How to use DI container when OwinStartup" you can find a lot of possibilities, but the one that I implemented and solved my problem, was this blog post:
https://damienbod.wordpress.com/2013/10/01/self-host-webapi-with-owin-and-unity/
I hope this can help anyone else.

No controller is found by Web API although controller exists

I created one solution with two projects: one is a class library with a Self Host Web API (created with the help of http://www.asp.net/web-api/overview/hosting-aspnet-web-api/self-host-a-web-api), the second is a windows service created with TopShelf. The purpose of this solution is to have a status report on the service with the use of Web API.
Everything works fine, but when I recreate my solution within a target solution the whole application does not work properly. The Windows Service seems to be working, but when I type localhost:8080/Test which is suppose to view OK (and it does in the separate test solution mentioned at the beginning) it throws an error (viewed as an xml):
Message: No HTTP resource was found that matches the request URI 'http://localhost:8080/Test'.
MessageDetail: No type was found that matches the controller named 'Report'.
There is a ReportController (inheriting from ApiController) in the project that contains the SelfHost but somehow it's "visible". I took a guess (a stupid guess, I believe) and moved it to the windows service project but it's also not working.
Can someone tell me what is the problem I'm facing? Why does it not see the controller if it has in a simple solution?
EDIT:
My routing looks like this:
var config = new HttpSelfHostConfiguration(String.Format("http://localhost:{0}", port));
config.Routes.MapHttpRoute("API Default", "{action}", new { controller = defaultControllerName });
where
defaultControllerName = "Report";
I am ashamed to admit it, but the reason why it didn't work lay in the controller class not having an access modifier. Making it public fixed the bug.
the Class and method must be public
public class PrintController: ApiController
{
//[HttpGet, Route("api/Print/Getp")]
public string Get()
{
var ob = new List<string>();
foreach (var item in File.ReadLines(#"c:\PrintService\pr.txt"))
{
string i = item;
ob.Add(i);
}
var json1 = JsonConvert.SerializeObject(ob);
return "ok";
}
}
and my route config is this:
_config.Routes.MapHttpRoute("DefaultHttpRoute", "api/{controller}");

Categories

Resources