Add a CLI to an ASP.NET Core web application that supports dependency injection, authorization and authentication with as little as two lines of code.
You should install WebCommandLine with NuGet:
Install-Package WebCommandLine
Or via the .NET command line interface (.NET CLI):
dotnet add package WebCommandLine
Either commands, from Package Manager Console or .NET Core CLI, will allow download and installation of WebCommandLine and all its required dependencies.
First, configure WebCommandLine to know where the commands are located, in the startup of your application:
varbuilder=WebApplication.CreateBuilder(args);//basic registrationbuilder.Services.AddWebCommandLine(typeof(MyClass));// Tells WebCommandLine which assembly to scan for console commands//Or you can customize its behaviorbuilder.Services.AddWebCommandLine(options =>{options.StaticFilesUrl="/MyWebAssets";//This will be the base path for static filesoptions.WebCliUrl="/MyWebCli";// cammand requests will goes to this endpoint// If true the JavaScript bjects with be automatically initialized, otherwise you have to manually inti window.cli object// You would typically set this value to false when you want to override the default httpHandleroptions.AutoInitJsInstance=false;// Copy and paste configurationoptions.EnableAutoCopy=true;// Enable select-to-copy functionalityoptions.EnableRightClickPaste=true;// Enable right-click-to-paste functionality},typeof(ShowTable).Assembly);//...app.UseWebCommandLine();Create a class that implements the IConsoleCommand interface and add the ConsoleCommandAttribute to the class definition:
[ConsoleCommand("echo","Echos back the first arg received")]publicclassEcho:IConsoleCommand{publicTask<ConsoleResult>RunAsync(CommandContextcontext,string[]args){if(args.Length!=0){returnTask.FromResult(newConsoleResult(args[0]));}returnTask.FromResult(ConsoleResult.CreateError("I didn't hear anything!"));}}Run the application and then press the CTRL + ` keys to launch the web command line.
You can also implement the abstract base class ConsoleCommandBase to support help text for your commands by passing '?' or help as argument when running your command:
[ConsoleCommand("greet","Returns a greeting message")]publicclassGreet:ConsoleCommandBase{publicoverrideConsoleResultHelp(){varsb=newStringBuilder("<table class='webcli-tbl'><tr><td colspan='3' class='webcli-val'>Lists available arguments</td></tr>");sb.Append("<tr><td class='webcli-lbl'>USAGE:</td><td colspan='2' class='webcli-val'>greet Nyron</td></tr>");sb.Append("</table>");returnnewConsoleResult(sb.ToString()){isHTML=true};}protectedoverrideTask<ConsoleResult>RunAsyncCore(CommandContextcontext,string[]args){if(args.Length==0){returnTask.FromResult(ConsoleResult.CreateError("Invalid argument pass"));}returnTask.FromResult(newConsoleResult($"Hello, {args[0]}. Nice to meet you!!"){isHTML=false});}}There is also support for strongly typed commands, using the build in argument parser or you can use your own parsing tool or logic.
publicclassAddMemberArguments{publicstringName{get;set;}publicintAge{get;set;}publicstringCategory{get;set;}}[ConsoleCommand("add-member","adds a new club member")]publicclassAddMember:ConsoleCommandBase<AddMemberArguments>{protectedreadonlyCommandLineParser<AddMemberArguments>_parser;publicAddMember(){_parser=newCommandLineParser<AddMemberArguments>();_parser.Bind(arg =>arg.Name).As('n',"name").Required();_parser.Bind(arg =>arg.Age).As('a',"age").WhereGreaterThan(18,"Must be over 18 to join!");_parser.Bind(arg =>arg.Category).As('c',"category").WhereIn(["basic","gold","platinum"]);}publicoverrideConsoleResultHelp(){varsb=newStringBuilder("<table class='webcli-tbl'><tr><td colspan='3' class='webcli-val'>Lists available arguments</td></tr>");sb.Append("<tr><td class='webcli-lbl'>-n | -name</td><td>:</td><td class='webcli-val'>Name that uniquely identifies member</td></tr>");sb.Append("<tr><td class='webcli-lbl'>-a</td><td>:</td><td class='webcli-val'>Age of member. Must be over 18</td></tr>");sb.Append("<tr><td class='webcli-lbl'>-c</td><td>:</td><td class='webcli-val'>Member category. Valid options includes: basic,gold, platinum</td></tr>");sb.Append("<tr><td class='webcli-lbl'>USAGE:</td><td colspan='2' class='webcli-val'>add-member -a 34 -c platinum -w 1</td></tr>");sb.Append("</table>");returnnewConsoleResult(sb.ToString()){isHTML=true};}protectedoverrideCommandLineParserResult<AddMemberArguments>Parse(string[]args){varresult=_parser.Parse(args);returnresult;}protectedoverrideTask<ConsoleResult>RunAsyncCore(CommandContextcontext,AddMemberArgumentsmodel){if(!model.Name.Equals("Jone Doe",StringComparison.OrdinalIgnoreCase)){returnTask.FromResult(newConsoleResult($"Member created successfully"));}else{returnTask.FromResult(ConsoleResult.CreateError("Invalid name"));}}}The example below shows how to implement a strongly typed command using Fluent Command Line Parser to parse the arguments:
publicclassAddUserArguments{publicstringUserName{get;set;}publicstringPassword{get;set;}publicList<string>Claims{get;set;}}[ConsoleCommand("add-user","adds a new user account")]publicclassAddUser:ConsoleCommandBase<AddUserArguments>{protectedreadonlyFluentCommandLineParser<AddUserArguments>_parser;publicAddUser(){_parser=newFluentCommandLineParser<AddUserArguments>();// specify which property the value will be assigned too._parser.Setup(arg =>arg.UserName).As('n',"userName")// define the short and long option name.Required();// using the standard fluent Api to declare this Option as required._parser.Setup(arg =>arg.Password).As('p',"password").SetDefault("P@$$w0rd");_parser.Setup(arg =>arg.Claims).As('c',"claims");}publicoverrideConsoleResultHelp(){//add-user -n userName -p password -c claims [optional]varsb=newStringBuilder("<table class='webcli-tbl'><tr><td colspan='3' class='webcli-val'>Lists available arguments</td></tr>");sb.Append("<tr><td class='webcli-lbl'>-n</td><td>:</td><td class='webcli-val'>Name that uniquely identifies user</td></tr>");sb.Append("<tr><td class='webcli-lbl'>-p</td><td>:</td><td class='webcli-val'>User password, Default will be used is not value is provided</td></tr>");sb.Append("<tr><td class='webcli-lbl'>-c</td><td>:</td><td class='webcli-val'>Claims that determine what functions the user can access. Valid options includes: reports,user, customer & webcli (optional)</td></tr>");sb.Append("<tr><td class='webcli-lbl'>USAGE:</td><td colspan='2' class='webcli-val'>add-user -n nyron.williams@willcorp.com -p MySecretPassword -c \"report,user,webcli\" -w 1</td></tr>");sb.Append("</table>");returnnewConsoleResult(sb.ToString()){isHTML=true};}protectedoverrideCommandLineParserResult<AddUserArguments>Parse(string[]args){varresult=_parser.Parse(args);returnnewCommandLineParserResult<AddUserArguments>(_parser.Object,result.ErrorText);}protectedoverrideTask<ConsoleResult>RunAsyncCore(CommandContextcontext,AddUserArgumentsuserToAdd){if(!userToAdd.UserName.Equals("foo",StringComparison.OrdinalIgnoreCase)){returnTask.FromResult(newConsoleResult($"User created successfully"));}else{returnTask.FromResult(ConsoleResult.CreateError("Invalid username"));}}}WebCommandLine leverages existing ASP.NET Authorization features and requires little effort for integration. The WebCommandLine endpoint can be secured by setting the Authorization property of the WebCommandLineConfiguration class when calling the AddWebCommandLine method during your application startup or You can simply add the add the Authorize attribute to the command class.
//...builder.Services.AddWebCommandLine(options =>{options.Authorization=new[]{newWebCommandLineAuthorization{Policy="AdminUser",Roles="Operator,Developer"}};});//...//Adding sample policiesbuilder.Services.AddAuthorization(options =>{options.AddPolicy("AdminUser", policyBuilder =>{policyBuilder.RequireAuthenticatedUser();policyBuilder.RequireClaim("isAdmin","true");});options.AddPolicy("PowerUser", policyBuilder =>{policyBuilder.RequireAuthenticatedUser();policyBuilder.AddRequirements(newWebCmdLineRequirement());policyBuilder.RequireAssertion(ctx =>{returnctx.User.IsInRole("BusinessAdmin");});});});//....[Authorize(Policy="PowerUser")][ConsoleCommand("greet","Returns a greeting message")]publicclassGreet:ConsoleCommandBase{publicoverrideConsoleResultHelp(){varsb=newStringBuilder("<table class='webcli-tbl'><tr><td colspan='3' class='webcli-val'>Lists available arguments</td></tr>");sb.Append("<tr><td class='webcli-lbl'>USAGE:</td><td colspan='2' class='webcli-val'>greet nyron</td></tr>");sb.Append("</table>");returnnewConsoleResult(sb.ToString()){isHTML=true};}protectedoverrideTask<ConsoleResult>RunAsyncCore(CommandContextcontext,string[]args){if(args.Length!=0)returnTask.FromResult(newConsoleResult($"Hello, {args[0]}. Nice to meet you!!"){isHTML=false});varuser=context.HttpContext.User;if(user!=null){varname=user.FindFirst("preferred_username")?.Value;if(!string.IsNullOrEmpty(name))returnTask.FromResult(newConsoleResult($"Hello, {name}. Nice to meet you!!"));}returnTask.FromResult(ConsoleResult.CreateError("Invalid argument pass"));}}You can change WebCommandLine url base paths by modifying the WebCommandLineConfiguration class during your application startup.
//...builder.Services.AddWebCommandLine(options =>{options.StaticFilesUrl="/MyWebAssets";//This will be the base path for static filesoptions.WebCliUrl="/MyWebCli";//command requests will go to this endpoint// If true the JavaScript objects with be automatically initialized, otherwise you have to manually inti window.cli object// You would typically set this value to false when you want to override the default httpHandleroptions.AutoInitJsInstance=false;});//...Client side code to override httpHandler
document.addEventListener("DOMContentLoaded",function(){functionajaxHttpHandler(endpoint,options){returnnewPromise((resolve,reject)=>{$.ajax({url: endpoint,method: options.method,headers: options.headers,data: options.body,success: function(data){resolve(data);// Return raw data as is},error: function(xhr){// Handle error response here (including parsing)reject(xhr.responseText||xhr.statusText);}});});}functionaxiosHttpHandler(endpoint,options){const{ method, headers, body }=options;returnaxios({url: endpoint,method: method,headers: headers,data: body,}).then(response=>response.data)// Return the response as raw text.catch(error=>{// Handle the error here (including parsing)returnPromise.reject(error.response?.data||error.message);});}window.cli=newWebCLI('/MyWebCli',ajaxHttpHandler);});2.0.0 - Added a new command context parameter to the ICommand interface and implementing base classes. This will enable greater flexibility and make the code more extendable. This new context currently exposes the current HttpContexc, which can be used to access the HttpRequest (headers, users, claims etc) which can make integrating with other areas of asp.net request pipeline much easier