I am trying to create a unit test cases using nunit on controllers in an existing Web API project.
I am using Moq as mocking framework. In here I would like to mock repository to return some expected
result.I am not sure what I can do at the point of setting up of repository mockup and validating result.
Can somebody suggest if whatever I am doing it right ? I just took a random controller for this question.
Any advise or guidance to some example url would be greatly appreciated.
[TestCase]
public void TestMethod1()
{
AccountController ac = new AccountController();
var mockAuthRepository = new Mock<AuthRepository>();
//mockAuthRepository.Setup(m=>m.RegisterUser(It.IsAny<UserModel>))
}
[RoutePrefix("api/Account")]
public class AccountController : ApiController
{
private readonly AuthRepository _repo;
public AccountController()
{
_repo = new AuthRepository();
}
}
[HttpPost]
[Authorize(Users = "admin")]
[Route("Register")]
public async Task<IHttpActionResult> Register(UserModel userModel)
{
IdentityResult result = await _repo.RegisterUser(userModel);
IHttpActionResult errorResult = WrapError(result);
if (errorResult != null)
{
return errorResult;
}
return Ok();
}
private IHttpActionResult WrapError(IdentityResult result)
{
if (result == null)
{
return InternalServerError();
}
if (!result.Succeeded)
{
if (result.Errors != null)
{
foreach (string err in result.Errors)
{
ModelState.AddModelError("", err);
}
}
if (ModelState.IsValid)
{
return BadRequest();
}
return BadRequest(ModelState);
}
return null;
}
In your any specific test use setups like these:
mockAuthRepository
.Setup(m => m.RegisterUser(It.IsAny<UserModel>))
.Returns(new List<UserModel> {
new UserModel { // specific properties },
new UserModel { // specific properties },
new UserModel { // specific properties }
}.AsQueryable());
And then just make assertion for selected users.
For example, you can test Register method to prevent registering existing user by placing in mockAuthRepository the same user that was in controller input and assert that specific error code is generated.
You can also use mocks to check if some code is called by using Verify method with param Times.Once, Times.Never etc..
Good luck!
Related
I'm having problems with seeding my inmemory database with test-data in nUnit integration tests.
I've created a custom WebApplicationFactory so i could use an inmemory database:
public class SiriusWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IAuthenticationSchemeProvider));
services.Remove(descriptor);
CleanupDatabaseRegistrations<SiriusContext>(services);
services.AddDbContext<SiriusContext>(options => options.UseInMemoryDatabase($"Testdb-{Guid.NewGuid()}"));
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database contexts
using var scope = sp.CreateScope();
var scopedServices = scope.ServiceProvider;
var context = scopedServices.GetRequiredService<SiriusContext>();
// Ensure the database is created.
context.Database.EnsureCreated();
});
}
private void CleanupDatabaseRegistrations<TDbContext>(IServiceCollection services) where TDbContext : DbContext
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<TDbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(DbContextOptions));
if (descriptor != null)
{
services.Remove(descriptor);
}
descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(TDbContext));
if (descriptor != null)
{
services.Remove(descriptor);
}
}
}
After this, i've created a base class which is being used by all my integration test classes.
This base class implements the nUnit OneTimeSetup method where i create the HttpClient for testing my controllers:
protected HttpClient HttpClient;
protected SiriusContext DbContext;
[OneTimeSetUp]
public void OneTimeSetUp()
{
_webApplicationFactory = new SiriusWebApplicationFactory();
HttpClient = _webApplicationFactory.CreateClient();
DbContext = _webApplicationFactory.Services.GetService<SiriusContext>();
DoReseed();
}
As you can see i'm exposing my DbContext so that my test classes can seed the test data.
The DoReseed method is just an emppty method used by my integration test classes to provide test data.
protected virtual void DoReseed() { }
The implementation inside one of my testclasses looks like this:
public class AdminAanbestedingApplicatieControllerTests : BaseIntegrationTest
{
[Test]
public async Task AanbestedingApplicaties_GetAll_ShouldReturnData()
{
var result = await HttpClient.GetFromApi_AssertSuccess<List<AanbestedingApplicatie>>(RouteConstants.Lijsten.AdminAanbestedingApplicaties.GetAll);
Assert.IsTrue(result.Count > 1);
}
protected override void DoReseed()
{
DbContext.AanbestedingApplicaties.Add(AanbestedingApplicatiesObjectMother.AanbestedingApplicatie_Actief);
DbContext.AanbestedingApplicaties.Add(AanbestedingApplicatiesObjectMother.AanbestedingApplicatie_InActief);
DbContext.AanbestedingApplicaties.Add(AanbestedingApplicatiesObjectMother.AanbestedingApplicatie_ToDelete);
DbContext.AanbestedingApplicaties.Add(AanbestedingApplicatiesObjectMother.AanbestedingApplicatie_ToUpdate);
DbContext.SaveChanges();
}
}
When i execute my test, the seeding gets executed but the repository cannot see the seeded data?
The DbContext inside my repo does not contain my seeded data...
Anyone has any idea why the DbContext inside my repository does not contain the seeded test data?
The repo looks like this:
public IEnumerable<AanbestedingApplicatie> GetAll()
{
return SiriusContext.AanbestedingApplicaties.OrderBy(l => l.Naam).ProjectTo<AanbestedingApplicatie>(Mapper.ConfigurationProvider)
.ToList();
}
The Context has been injected inside the constructor...
First the error:
Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and
then later trying to use the same context instance elsewhere in your
application. This may occur if you are calling Dispose() on the
context, or wrapping the context in a using statement. If you are
using dependency injection, you should let the dependency injection
container take care of disposing context instances.
Object name: 'MemberContext'.
I have 3 projects, Domain, API and WebSPA app.
Domain has 2 modules, DomainModule and MediatorModule
public class DomainModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(typeof(MemberContext).Assembly)
.AsImplementedInterfaces()
.InstancePerLifetimeScope(); // via assembly scan
builder.RegisterType<MemberContext>().AsSelf()
.InstancePerLifetimeScope(); // or individually
}
}
public class MediatorModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
// enables contravariant Resolve() for interfaces with single contravariant ("in") arg
builder
.RegisterSource(new ContravariantRegistrationSource());
// mediator itself
builder
.RegisterType<Mediator>()
.As<IMediator>()
.InstancePerLifetimeScope();
// request handlers
builder
.Register<SingleInstanceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t =>
{
object o;
return c.TryResolve(t, out o) ? o : null;
};
})
.InstancePerLifetimeScope();
// notification handlers
builder
.Register<MultiInstanceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t => (IEnumerable<object>) c.Resolve(typeof(IEnumerable<>).MakeGenericType(t));
})
.InstancePerLifetimeScope();
}
}
In API project I have also 2 modules, ApplicationModule and again MediatorModule same as the one above.
public class ApplicationModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(typeof(Startup).Assembly)
.AsImplementedInterfaces()
.InstancePerLifetimeScope(); // via assembly scan
builder.RegisterType<MemberContext>().AsSelf().InstancePerLifetimeScope(); // or individually
}
}
No, when I debug I can see that member context gets newed up on each request, yet on second request, it throws above error. To make sure I am not going crazy, I modified constructor of dbcontext to create an id for context so i can verify they are different. What am I doing wrong?
public MemberContext(DbContextOptions<MemberContext> options) : base(options)
{
MemberContextId = Guid.NewGuid();
Console.WriteLine("member context created: " + MemberContextId);
}
Here is the startup in API
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
// .AllowCredentials()
);
});
services.AddMvc()
.AddControllersAsServices();//Injecting Controllers themselves thru DI
//For further info see: http://docs.autofac.org/en/latest/integration/aspnetcore.html#controllers-as-services
AddSwaggerGen(services);
//var connection = Configuration["ConnectionString"];
//services.AddDbContext<MemberContext>(options => options.UseSqlServer(connection),ServiceLifetime.Scoped);
services.AddEntityFrameworkSqlServer()
.AddDbContext<MemberContext>(options =>
{
options.UseSqlServer(Configuration["ConnectionString"]
//,sqlServerOptionsAction: sqlOptions =>
//{
// sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
// sqlOptions.EnableRetryOnFailure(maxRetryCount: 10, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
//}
);
},
ServiceLifetime.Scoped //Showing explicitly that the DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request)
);
var container = new ContainerBuilder();
container.Populate(services);
container.RegisterAssemblyModules(typeof(VIN.Members.Domain.Entities.Member).Assembly,
typeof(Startup).Assembly);
return new AutofacServiceProvider(container.Build());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//NOTE: must be before UseMVC !!!
app.UseCors("CorsPolicy");
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
}
private void AddSwaggerGen(IServiceCollection services)
{
services.AddSwaggerGen(options =>
{
options.DescribeAllEnumsAsStrings();
options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info
{
Title = "VIN Members HTTP API",
Version = "v1",
Description = "Members Service HTTP API",
TermsOfService = "Terms Of Service"
});
});
}
}
UPDATE:
What I am trying to do is delete a record. On client side code looks like this
onDelete(item: IMember) {
//TODO: replace this with dialog service component
if (window.confirm('Are sure you want to delete this member?')) {
//put your delete method logic here
this.service.deleteMember(item).subscribe(x => {
this.getMembers();
});
}
}
this delete request gets mapped to a controller that passes it to mediator
Controller
// DELETE api/members/5
[HttpDelete("{id}")]
public void Delete(Guid id)
{
var command = new DeleteMember.Command(id);
_mediator.Send(command).ConfigureAwait(false);
}
and finally handler
public class DeleteMember
{
public class Command : IRequest
{
public Command(Guid memberId)
{
Guard.NotNull(memberId, nameof(memberId));
MemberId = memberId;
}
public Guid MemberId { get; }
}
public class Handler : AsyncRequestHandler<Command>
{
private MemberContext _context;
public Handler(MemberContext context)
{
_context = context;
Console.WriteLine("Delete member context: " + context.MemberContextId);
}
protected override async Task HandleCore(Command cmd)
{
try
{
var member = await _context.FindAsync<Member>(cmd.MemberId);//.ConfigureAwait(false);
// if (member != null)
//// {
_context.Remove(member);
await _context.SaveChangesAsync().ConfigureAwait(false);
// }
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
}
As you can see there is no code that disposes that context. Scratching my head.
See this commented out check for member if null. That was throwing error as well, I commented it out just to see what will happen, and now it throws as SaveChangesAsync.
As request completes, context gets disposed. Since command handler uses SaveChangesAsync(), context is disposed before save completes. Culprit is controller method :). It should be async as well.
[HttpDelete("{id}")]
public async Task Delete(Guid id)
{
var command = new DeleteMember.Command(id);
await _mediator.Send(command).ConfigureAwait(false);
}
Your DbContext is scoped, meaning that Dependency Injection will return the same DbContext object every time one is asked for within the same HTTP request (in the case of ASP.NET).
That means that you should not be calling Dispose on your DbContext (otherwise that same object can't be used a second time). That seems to be what is happening to you, intentionally or not.
That does mean you should not be using using with it. Are you using using anywhere in your code against your DbContext?
I don't think you showed the line where the Exception is being thrown.
Update:
Try overriding Dispose in your MemberContext class. Something like this:
public override void Dispose() {
base.Dispose();
}
But just set a breakpoint there. When it breaks (if it does) check the stack trace and see what called it.
This can also be caused by having async void instead of async Task within WebAPI in my experience.
The project I'm working on has the Entity Framework on top of an OData layer. The Odata layer has it's server side pagination turned to a value of 75. My reading on the subject leads me to believe that this pagination value is used across the board, rather than a per table basis. The table that I'm currently looking to extract all the data from is, of course, more than 75 rows. Using the entity framework, my code is simply thus:
public IQueryable<ProductColor> GetProductColors()
{
return db.ProductColors;
}
where db is the entity context. This is returning the first 75 records. I read something where I could append a parameter inlinecount set to allpages giving me the following code:
public IQueryable<ProductColor> GetProductColors()
{
return db.ProductColors.AddQueryOption("inlinecount","allpages");
}
However, this too returns 75 rows!
Can anyone shed light on how to truly get all the records regardless of the OData server-side pagination stuff?
important: I cannot remove the pagination or turn it off! It's extremely valuable in other scenarios where performance is a concern.
Update:
Through some more searching I've found an MSDN that describes how to do this task.
I'd love to be able to turn it into a full Generic method but, this was as close as I could get to a generic without using reflection:
public IQueryable<T> TakeAll<T>(QueryOperationResponse<T> qor)
{
var collection = new List<T>();
DataServiceQueryContinuation<T> next = null;
QueryOperationResponse<T> response = qor;
do
{
if (next != null)
{
response = db.Execute<T>(next) as QueryOperationResponse<T>;
}
foreach (var elem in response)
{
collection.Add(elem);
}
} while ((next = response.GetContinuation()) != null);
return collection.AsQueryable();
}
calling it like:
public IQueryable<ProductColor> GetProductColors()
{
QueryOperationResponse<ProductColor> response = db.ProductColors.Execute() as QueryOperationResponse<ProductColor>;
var productColors = this.TakeAll<ProductColor>(response);
return productColors.AsQueryable();
}
If unable turn off paging you'll receive 75 row by call, always. You can get all rows in following ways:
Add another IQueryable<ProductColor> AllProductColors and modify
public static void InitializeService(DataServiceConfiguration config)
{
config.UseVerboseErrors = true;
config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
config.SetEntitySetPageSize("ProductColors", 75); - Note only paged queries are present
config.SetServiceOperationAccessRule("*", ServiceOperationRights.AllRead);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
You should call ProductColors as many as needed, for example
var cat = new NetflixCatalog(new Uri("http://odata.netflix.com/v1/Catalog/"));
var x = from t in cat.Titles
where t.ReleaseYear == 2009
select t;
var response = (QueryOperationResponse<Title>)((DataServiceQuery<Title>)x).Execute();
while (true)
{
foreach (Title title in response)
{
Console.WriteLine(title.Name);
}
var continuation = response.GetContinuation();
if (continuation == null)
{
break;
}
response = cat.Execute(continuation);
}
I use Rx with following code
public sealed class DataSequence<TEntry> : IObservable<TEntry>
{
private readonly DataServiceContext context;
private readonly Logger logger = LogManager.GetCurrentClassLogger();
private readonly IQueryable<TEntry> query;
public DataSequence(IQueryable<TEntry> query, DataServiceContext context)
{
this.query = query;
this.context = context;
}
public IDisposable Subscribe(IObserver<TEntry> observer)
{
QueryOperationResponse<TEntry> response;
try
{
response = (QueryOperationResponse<TEntry>)((DataServiceQuery<TEntry>)query).Execute();
if (response == null)
{
return Disposable.Empty;
}
}
catch (Exception ex)
{
logger.Error(ex);
return Disposable.Empty;
}
var initialState = new State
{
CanContinue = true,
Response = response
};
IObservable<TEntry> sequence = Observable.Generate(
initialState,
state => state.CanContinue,
MoveToNextState,
GetCurrentValue,
Scheduler.ThreadPool).Merge();
return new CompositeDisposable(initialState, sequence.Subscribe(observer));
}
private static IObservable<TEntry> GetCurrentValue(State state)
{
if (state.Response == null)
{
return Observable.Empty<TEntry>();
}
return state.Response.ToObservable();
}
private State MoveToNextState(State state)
{
DataServiceQueryContinuation<TEntry> continuation = state.Response.GetContinuation();
if (continuation == null)
{
state.CanContinue = false;
return state;
}
QueryOperationResponse<TEntry> response;
try
{
response = context.Execute(continuation);
}
catch (Exception)
{
state.CanContinue = false;
return state;
}
state.Response = response;
return state;
}
private sealed class State : IDisposable
{
public bool CanContinue { get; set; }
public QueryOperationResponse<TEntry> Response { get; set; }
public void Dispose()
{
CanContinue = false;
}
}
}
so for get any data thru OData, create a sequence and Rx does the rest
var sequence = new DataSequence<Product>(context.Products, context);
sequence.OnErrorResumeNext(Observable.Empty<Product>())
.ObserveOnDispatcher().SubscribeOn(Scheduler.NewThread).Subscribe(AddProduct, logger.Error);
The page size is set by the service author and can be set per entity set (but a service may choose to apply the same page size to all entity sets). There's no way to avoid it from the client (which is by design since it's a security feature).
The inlinecount option asks the server to include the total count of the results (just the number), it doesn't disable the paging.
From the client the only way to read all the data is to issue the request which will return the first page and it may contain a next link which you request to read the next page and so on until the last response doesn't have the next link.
If you're using the WCF Data Services client library it has support for continuations (the next link) and a simple sample can be found in this blog post (for example): http://blogs.msdn.com/b/phaniraj/archive/2010/04/25/server-driven-paging-with-wcf-data-services.aspx
Does the ASP.NET MVC 2 Default View Model Binding support binding a multi-value cookie to a custom object? Before I write a custom Value Provider, I would like to be sure that the functionality didn't already exist.
So given an action like:
public ActionResult SomeAction(CustomObject foo)
where CustomObject is something like:
public class CustomObject
{
public string Name { get; set; }
public int Rank { get; set; }
}
and a cookie that is part of the request like:
foo=Name=John&Rank=10
Could I get the Default View Model Binding to map the cookie to the parameter with some clever tweaks to the naming of the cookie or cookie values like posting "foo.Name=John" and "foo.Rank=10" would do?
Well, there's one way to do it would be to implement IModelBinder
public class CustomObjectModelBinder : IModelBinder {
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
HttpCookie c = controllerContext.HttpContext.Request.Cookies["foo"]
CustomObject value = new CustomObject() {
foo.Name = c.Values["Name"],
foo.Rank = c.Values["Rank"]
}
return CustomObject
}
}
Then just add this to your Application_Start()
ModelBinders.Binders.Add(typeof(CustomObject), new CustomObjectModelBinder());
you can add the cookie object to any action as far as i know and it will attempt to bind it for you
In the end I created something to do this. Based on the work posted by Mehdi Golchin, I created a value provider that allows this kind of binding to happen.
For those interrested, the following are the custom changes I made to Mehdi's work linked above. See the link for full details on implementation. This doesn't support binding to nested objects (e.g., Foo.Cell.X) because I didn't need that level of complexity, but it would be possible to implement with a bit of recursion.
protected virtual bool ContainsPrefix(string prefix)
{
try
{
var parts = prefix.Split(new char[] { '.' }, 2, StringSplitOptions.RemoveEmptyEntries);
switch (parts.Length)
{
case 0:
return false;
case 1:
return this._context.HttpContext.Request.Cookies.AllKeys.Contains(parts[0]);
default:
var cookie = this._context.HttpContext.Request.Cookies[parts[0]];
if (cookie == null) { return false; }
return cookie.Values.AllKeys.Contains(parts[1]);
}
}
catch (Exception ex)
{
ExceptionPolicy.HandleException(ex, "Controller Policy");
return false;
}
}
protected virtual ValueProviderResult GetValue(string key)
{
try
{
var parts = key.Split(new char[] { '.' }, 2, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2) { return null; }
var cookie = this._context.HttpContext.Request.Cookies[parts[0]];
if (cookie == null) { return null; }
var value = cookie.Values[parts[1]];
if (value == null) { return null; }
return new ValueProviderResult(value, value, CultureInfo.CurrentCulture);
}
catch (Exception ex)
{
ExceptionPolicy.HandleException(ex, "Controller Policy");
return null;
}
}
In Asp.net MVC the url structure goes like
http://example.com/{controller}/{action}/{id}
For each "controller", say http://example.com/blog, there is a BlogController.
But my {controller} portion of the url is not decided pre-hand, but it is dynamically determined at run time, how do I create a "dynamic controller" that maps anything to the same controller which then based on the value and determines what to do?
Same thing with {action}, if the {action} portion of my url is also dynamic, is there a way to program this scenario?
Absolutely! You'll need to override the DefaultControllerFactory to find a custom controller if one doesn't exist. Then you'll need to write an IActionInvoker to handle dynamic action names.
Your controller factory will look something like:
public class DynamicControllerFactory : DefaultControllerFactory
{
private readonly IServiceLocator _Locator;
public DynamicControllerFactory(IServiceLocator locator)
{
_Locator = locator;
}
protected override Type GetControllerType(string controllerName)
{
var controllerType = base.GetControllerType(controllerName);
// if a controller wasn't found with a matching name, return our dynamic controller
return controllerType ?? typeof (DynamicController);
}
protected override IController GetControllerInstance(Type controllerType)
{
var controller = base.GetControllerInstance(controllerType) as Controller;
var actionInvoker = _Locator.GetInstance<IActionInvoker>();
if (actionInvoker != null)
{
controller.ActionInvoker = actionInvoker;
}
return controller;
}
}
Then your action invoker would be like:
public class DynamicActionInvoker : ControllerActionInvoker
{
private readonly IServiceLocator _Locator;
public DynamicActionInvoker(IServiceLocator locator)
{
_Locator = locator;
}
protected override ActionDescriptor FindAction(ControllerContext controllerContext,
ControllerDescriptor controllerDescriptor, string actionName)
{
// try to match an existing action name first
var action = base.FindAction(controllerContext, controllerDescriptor, actionName);
if (action != null)
{
return action;
}
// #ray247 The remainder of this you'd probably write on your own...
var actionFinders = _Locator.GetAllInstances<IFindAction>();
if (actionFinders == null)
{
return null;
}
return actionFinders
.Select(f => f.FindAction(controllerContext, controllerDescriptor, actionName))
.Where(d => d != null)
.FirstOrDefault();
}
}
You can see a lot more of this code here. It's an old first draft attempt by myself and a coworker at writing a fully dynamic MVC pipeline. You're free to use it as a reference and copy what you want.
Edit
I figured I should include some background about what that code does. We were trying to dynamically build the MVC layer around a domain model. So if your domain contained a Product class, you could navigate to products\alls to see a list of all products. If you wanted to add a product, you'd navigate to product\add. You could go to product\edit\1 to edit a product. We even tried things like allowing you to edit properties on an entity. So product\editprice\1?value=42 would set the price property of product #1 to 42. (My paths might be a little off, I can't recall the exact syntax anymore.) Hope this helps!
After a little more reflection, there may be a bit simpler way for you to handle the dynamic action names than my other answer. You'll still need to override the default controller factory. I think you could define your route like:
routes.MapRoute("Dynamic", "{controller}/{command}/{id}", new { action = "ProcessCommand" });
Then on your default/dynamic controller you'd have
public ActionResult ProcessCommand(string command, int id)
{
switch(command)
{
// whatever.
}
}
You need to write your own IControllerFactory (or perhaps derive from DefaultControllerFactory) and then register it with ControllerBuilder.
Iam working with it in .Core but i'll share it's MVC version for all, after that i will share the core version
case OwnerType.DynamicPage:
var dp = mediator.Handle(new Domain.DynamicPages.DynamicPageDtoQuery { ShopId = ShopId, SeoId = seoSearchDto.Id }.AsSingle());
if (dp != null)
{
return GetDynamicPage(dp.Id);
}
break;
// some codes
private ActionResult GetDynamicPage(int id)
{
var routeObj = new
{
action = "Detail",
controller = "DynamicPage",
id = id
};
var bController = DependencyResolver.Current.GetService<DynamicPageController>();
SetControllerContext(bController, routeObj);
return bController.Detail(id);
}
// and
private void SetControllerContext(ControllerBase controller, object routeObj)
{
RouteValueDictionary routeValues = new RouteValueDictionary(routeObj);
var vpd = RouteTable.Routes["Default"].GetVirtualPath(this.ControllerContext.RequestContext, routeValues);
RouteData routeData = new RouteData();
foreach (KeyValuePair<string, object> kvp in routeValues)
{
routeData.Values.Add(kvp.Key, kvp.Value);
}
foreach (KeyValuePair<string, object> kvp in vpd.DataTokens)
{
routeData.DataTokens.Add(kvp.Key, kvp.Value);
}
routeData.Route = vpd.Route;
if (routeData.RouteHandler == null)
routeData.RouteHandler = new MvcRouteHandler();
controller.ControllerContext = new ControllerContext(this.ControllerContext.HttpContext, routeData, controller);
}