Manipulating the controller map for an API application - yii2

I have a set of extensions in my application (yii2-advanced) that have to be maintained in their own repos. To make them as detached as possible, they register as modules on their own and adds to the menu and migrations like this:
vendor\ext\Ext.php
namespace vendor\ext;
use yii\base\BootstrapInterface;
class ext implements BootstrapInterface
{
public function bootstrap($app)
{
$this->addToMenu($app);
$this->addToModules($app);
$app->params['migrations'] = array_merge($app->params['migrations'], ['#vendor/ext/migrations']);
}
private function addToMenu($app)
{
...
}
private function addToModules($app)
{
$app->setModule('ext', ['class' => 'vendor\ext\Module']);
}
}
vendor\ext\composer.json
"extra": {
"bootstrap": "vendor\\ext\\Ext"
},
This works very well, with the controllers in vendor\ext\controllers\. I have an extra application created as a REST API, and I need that application to access vendor\ext\api\ instead of vendor\ext\controllers\.
So if you'd access example.com/ext/controller you'd get vendor\ext\controllers\controller::index(), but if you'd access api.example.com/ext/controller you'd get vendor\ext\api\controller::index().
I've read trough the docs a lot to solve the bootstrapping functionalities that I have, but I can't seem to figure out this one.

Perhaps you can map the controller:
https://www.yiiframework.com/doc/guide/2.0/en/rest-routing#routing
As per example in that documentation:
[
'class' => 'yii\rest\UrlRule',
'controller' => ['u' => 'user'],
]
You could use bootstrapping to create the rules in the UrlManager.
Usually I use strict parsing in Rest API's, so we have to define our rules explicitly anyway.

I solved it by adding each API version as another module and register it with the corresponding version module of my API application.
Now I can develop each module with its supported api versions in a separate repository and everything registers automatically.
Posting the bootstrap code below if anyone needs do do something similar.
public $supported_api_versions = [];
public function bootstrap($app)
{
$this->addToMenu($app);
if($app->id == 'app-frontend')
{
$this->addToModules($app);
}
elseif($app->id == 'app-backend')
{
// Add code here if the module should be available in backend application
}
elseif($app->id == 'app-api')
{
foreach ( $this->supported_api_versions as $api ) {
$module = $app->getModule($api);
$module->setModule($this->name, ['class' => 'vendor\\'.$this->name.'\api\\'.$api.'\Module']);
}
}
elseif($app->id == 'app-console')
{
$app->params['migrations'] = array_merge($app->params['migrations'], ['#vendor/'.$this->name.'/migrations']);
}
}

Related

How to load app configuration from appsettings.json in MAUI startup?

I would need to retrieve my app settings from an appsettings.json file in a MAUI application.
I tagged it a MauiAsset generation action, and I can see it in the assets directory of of the generated apk.
It doesn't seem to be available in ConfigureAppConfiguration and no AddJsonFile extension exists to add it in the application builder.
Should I use another generation action?
What is the good way to retrieve it?
public void Configure(IAppHostBuilder appBuilder)
{
appBuilder
.ConfigureAppConfiguration((app, config) =>
{
// I should be able to work with appsettings here
})
.ConfigureServices(svcs =>
{
})
.UseMauiApp<App>();
}
It looks like the ConfigureAppConfiguration was just added with this PR a couple of days ago. It will probably show up for the next release.
If you really need it now you could add it as an embedded resource and do the deserialization from JSON yourself. If you could wait a bit longer you probably want to do that.
It's possible to get the usual host builder by using the MauiAppBuilder.Host getter.
var builder = MauiApp.CreateBuilder();
builder
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
})
.UseMauiApp<App>()
.Host
.ConfigureAppConfiguration((app, config) =>
{
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
})
.ConfigureServices((ctx, svcs) =>
{
})
.ConfigureLogging(logging =>
{
logging.AddSerilog();
});
To get app location and appsetings path, use the following:
Assembly CallingAssembly = Assembly.GetEntryAssembly();
Version VersionRuntime = CallingAssembly.GetName().Version;
string AssemblyLocation = Path.GetDirectoryName(CallingAssembly.Location);
string ConfigFile = Path.Combine(AssemblyLocation, "appsettings.json");

EmbedIO with a list of unknown web APIs

I want to have a modular EmbedIO setup with a dynamic list of unknown web API controller types. I thought it'd be easy :( But at the moment I'm stuck at registering the web APIs:
// Some APIs to setup at the EmbedIO webserver in "Server"
Dictionary<string, WebApiController> apis = ...;
// Register the APIs at the webserver
foreach(KeyValuePair<string, WebApiController> kvp in apis)
{
// Exception: "Controller type must be a subclass of WebApiController."
Server.WithWebApi(kvp.Key, m => m.WithController(() => kvp.Value));
}
The problem is: The factory method needs to return the final type of the controller object. Everything else seems to fail.
I tried with dynamic instead of WebApiController or returning object and giving the type as first parameter for WithController - whatever I tried, it resulted in an exception; Or when I use a class WebApiControllerWrapper : WebApiController and a Dictionary<string, WebApiControllerWrapper>, the exported controller methods of the final type are missing, because they're not defined in WebApiControllerWrapper.
It seems the only way is to use reflection for the generic call of WithController - or does anyone know another working solution (I'm in .NET Standard 2.1)?
I was able to solve it with an expression tree that calls a generic method to create the factory function:
public class ModularWebApiController : WebApiController
{
public Func<T> CreateFactoryMethod<T>() where T : WebApiController => () => (T)this;
}
public static class Extensions
{
public static WebApiModule WithController(this WebApiModule webApiModule, ModularWebApiController api)
{
Delegate factoryFunc = Expression
.Lambda(Expression.Call(
Expression.Constant(api),
typeof(ModularWebApiController).GetMethod("CreateFactoryMethod").MakeGenericMethod(api.GetType())
))
.Compile();
return (WebApiModule)typeof(WebApiModuleExtensions)
.GetMethods(BindingFlags.Static | BindingFlags.Public)
.Single(mi => mi.IsGenericMethod & mi.Name == "WithController" && mi.GetParameters().Length == 2)
.MakeGenericMethod(api.GetType())
.Invoke(null, new object[] { webApiModule, factoryFunc.DynamicInvoke(Array.Empty<object>()) });
}
}
I only had to ensure that all web API controller types extend the ModularWebApiController type, and I had to change the modular web API setup for EmbedIO:
Dictionary<string, ModularWebApiController> apis = ...;
foreach(KeyValuePair<string, ModularWebApiController> kvp in apis)
{
Server.WithWebApi(kvp.Key, m => m.WithController(kvp.Value));
}
After browsing the EmbedIO source I think this seems to be the only way to have a modular web API setup, where the code doesn't know which web API controller types are going to be used.
Now I'm able to load and instance any web API controller type configured in a JSON configuration file like this:
[
{
"Type": "name.space.WebApiControllerTypeName",
"Path": "/webapipath/"
},
{
"Type": "name.space.AnotherWebApiControllerTypeName",
"Path": "/anotherwebapipath/"
}
]
Just for example. I wonder why it seems that nobody else want to do this ;)

Razor page can't see referenced class library at run time in ASP.NET Core RC2

I started a new MVC Web Application project for the RC2 release and I'm trying to add a class library as a project reference.
I added a simple class library to my project and referenced it and got the following in the project.json file:
"frameworks": {
"net452": {
"dependencies": {
"MyClassLibrary": {
"target": "project"
}
}
}
},
I can use this library in any of the Controllers and the Startup.cs files without any trouble but I get the following error at run time when I try and use the library from a Razor page:
The name 'MyClassLibrary' does not exist in the current context
Output.WriteLine(MyClassLibrary.MyStaticClass.SomeStaticString);
It's weird because I'm getting intellisense for the class library when I'm editing the Razor page, and I can't find anything that says you can't use project references from here.
I thought it was hard enough getting this running under RC1 with the "wrap folder" in the class library project but this has me stumped.
A workaround has been posted on the issue page (cred to pranavkm and patrikwlund)
https://github.com/aspnet/Razor/issues/755
Apparently you need to explicitly add references to Razor compilation using RazorViewEngineOptions.CompilationCallback.
Add the following to your ConfigureServices method in your Startup class:
var myAssemblies = AppDomain.CurrentDomain.GetAssemblies().Select(x => MetadataReference.CreateFromFile(x.Location)).ToList();
services.Configure((RazorViewEngineOptions options) =>
{
var previous = options.CompilationCallback;
options.CompilationCallback = (context) =>
{
previous?.Invoke(context);
context.Compilation = context.Compilation.AddReferences(myAssemblies);
};
});
I had to filter out dynamic assemblies to avoid this runtime exception:
The invoked member is not supported in a dynamic assembly.
This worked for me:
var myAssemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(x => !x.IsDynamic)
.Select(x => Microsoft.CodeAnalysis.MetadataReference.CreateFromFile(x.Location))
.ToList();
services.Configure((Microsoft.AspNetCore.Mvc.Razor.RazorViewEngineOptions options) => {
var previous = options.CompilationCallback;
options.CompilationCallback = (context) => {
previous?.Invoke(context);
context.Compilation = context.Compilation.AddReferences(myAssemblies);
};
});

Upgrading Laravel 4.2 to 5.0, getting [ReflectionException] Class App\Http\Controllers\PagesController does not exist

I was updating my project from laravel 4.2 to laravel 5.0. But, after I am facing this error and have been trying to solve it for the past 4 hours.
I didn't face any error like this on the 4.2 version. I have tried composer dump-autoload with no effect.
As stated in the guide to update, I have shifted all the controllers as it is, and made the namespace property in app/Providers/RouteServiceProvider.php to null. So, I guess all my controllers are in global namespace, so don't need to add the path anywhere.
Here is my composer.json:
"autoload": {
"classmap": [
"app/console/commands",
"app/Http/Controllers",
"app/models",
"database/migrations",
"database/seeds",
"tests/TestCase.php"
],
Pages Controller :
<?php
class PagesController extends BaseController {
protected $layout = 'layouts.loggedout';
public function getIndex() {
$categories = Category::all();
$messages = Message::groupBy('receiver_id')
->select(['receiver_id', DB::raw("COUNT('receiver_id') AS total")])
->orderBy('total', 'DESC'.....
And, here is BaseController.
<?php
class BaseController extends Controller {
//Setup the layout used by the controller.
protected function setupLayout(){
if(!is_null($this->layout)) {
$this->layout = View::make($this->layout);
}
}
}
In routes.php, I am calling controller as follows :
Route::get('/', array('as' => 'pages.index', 'uses' => 'PagesController#getIndex'));
Anyone please help. I have been scratching my head over it for the past few hours.
Routes are loaded in the app/Providers/RouteServiceProvider.php file. If you look in there, you’ll see this block of code:
$router->group(['namespace' => $this->namespace], function($router)
{
require app_path('Http/routes.php');
});
This prepends a namespace to any routes, which by default is App\Http\Controllers, hence your error message.
You have two options:
Add the proper namespace to the top of your controllers.
Load routes outside of the group, so a namespace isn’t automatically prepended.
I would go with option #1, because it’s going to save you headaches in the long run.

Execute my code before any action of any controller

I would like to check if my user have filled certain fields in his profile before he can access any action of any controller.
For example
if(empty(field1) && empty(field2))
{
header("Location:/site/error")
}
In yii1 I could do it in protected\components\Controller.php in init() function
But in yii2 I'm not sure where to put my code. I cannot modify core files, but not sure what to do in backend of my advanced application to make it work.
I know I can user beforeAction() but I have too many controllers to do that and to keep track of every controller
In case you need to execute a code before every controller and action, you can do like below:
1 - Add a component into your components directory, for example(MyGlobalClass):
namespace app\components;
class MyGlobalClass extends \yii\base\Component{
public function init() {
echo "Hi";
parent::init();
}
}
2 - Add MyGlobalClass component into your components array in config file:
'components' => [
'MyGlobalClass'=>[
'class'=>'app\components\MyGlobalClass'
],
//other components
3 - Add MyGlobalClass into bootstarp array in config file:
'bootstrap' => ['log','MyGlobalClass'],
Now, you can see Hi before every action.
Please note that, if you do not need to use Events and Behaviors you can use \yii\base\Object instead of \yii\base\Component
Just add in config file into $config array:
'on beforeAction' => function ($event) {
echo "Hello";
},
Create a new controller
namespace backend\components;
class Controller extends \yii\web\Controller {
public function beforeAction($event)
{
..............
return parent::beforeAction($event);
}
}
All your controllers should now extend backend\components\Controller and not \yii\web\Controller. with this, you should modify every controller. I would go for this solution.
I believe you might also replace 1 class with another (so no change to any controller necessary), something like
\Yii::$classMap = array_merge(\Yii::$classMap,[
'\yii\web\Controller'=>'backend\components\Controller',
]);
See more details here: http://www.yiiframework.com/doc-2.0/guide-tutorial-yii-integration.html and I took the code from here: https://github.com/mithun12000/adminUI/blob/master/src/AdminUiBootstrap.php
you can put this in your index.php file. However, make sure you document this change very well as somebody that will come and try to debug your code will be totally confused by this.
Just i think this code on config file can help you:
'on beforeAction' => function ($event) {
// To log all request information
},
'components' => [
'response' => [
'on beforeSend' => function($event) {
// To log all response information
},
],
];
Or, https://github.com/yiisoft/yii2/blob/master/docs/guide/security-authorization.md use RBAC, to restrict access to controllers actions one at a time based on rules. Why would you want to restrict access to controller actions based on user fields is beyond me. You will not be able to access anything (including the login form) if you put a restriction there.