We were using Refit on one of your API's to create and share a Client package for that API.
ICategoryApi.cs
[Post("/category")]
Task CreateCategoryAsync([Body] CategoryCommandDto createCategoryCommandDto);
and everything was working fine with a controller like
CategoryController.cs
[ApiController]
[Route("[controller]")]
public class CategoriesController : ControllerBase
{
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.Created)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> CreateCategory([FromBody] CategoryCommandDto createCategoryCommandDto)
{
//some code
}
}
The problem is that now we've added api versioning, and we choosed to version by route.
So now the endpoint /category looks like /v1/category and we will create a /v2/category soon.
Is there a way to configure refit (through attributes or similar) for it to understand my versioned routes?
I want to avoid having to write a new client for every new version of the API and including the version in the endpoint route like
ICategoryApiV1.cs
[Post("/v1/category")]
Task CreateCategoryAsync([Body] CategoryCommandDto createCategoryCommandDto);
Imagine that the client is bigger and has a lot of methods, not just one. Also not all the methods may change between versions.
You can achieve this in a different way:
1) Use like an argument from method;
ICategoryApiV1.cs
[Post("/{v}/category")]
Task CreateCategoryAsync([Body] CategoryCommandDto createCategoryCommandDto, [AliasAs("v")]int version = 1);
2) Define a property inside of CategoryCommandDto;
public class CategoryCommandDto
{
// Properties can be read-only and [AliasAs] isn't required
public int v { get { return 1; } }
.....
}
3) Define a baseUrl for httpClient during ICategoryApi creation
services.AddRefitClient<ICategoryApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri($"https://api.example.com/{version}"));
4) Or if you need some advanced calculation you can add a custom HttpHandler and configure inside yours client.
services.AddRefitClient<ICategoryApi>(settings)
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
.AddHttpMessageHandler<VersionHandler>()
Related
I have a controller action:
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null) { }
I need to be able to call this endpoint either through a razor page or using the HttpClient's PostAsJsonAsync.
I got this working for the razor page. But when I do:
var response = await client.PostAsJsonAsync($"{api}Account/Register", new { email, password });
I get a 415 Unsupported Media Type.
How can I support both ways of submitting the data?
The new behavior of model binding designed in asp.net core is a bit inconvenient in some cases (for some developers) but I personally think it's necessary to reduce some complexity in the binding code (which can improve the performance a bit). The new behavior of model binding reflects clearly the purpose of the endpoints (MVC vs Web API). So usually your web browsers don't consume Web API directly (in case you use it, you must follow its rule, e.g: post the request body with application/json content-type).
If you look into the source code of asp.net core, you may have the best work-around or solution to customize it to support the multi-source model binding again. I did not look into any source code but I've come up with a simple solution to support model binding from either form or request body like this. Note that it will bind data from either the form or request body, not combined from both.
The idea is just based on implementing a custom IModelBinder and IModelBinderProvider. You can wrap the default one (following the decorator pattern) and add custom logic around. Otherwise you need to re-implement the whole logic.
Here is the code
//the custom IModelBinder, this supports binding data from either form or request body
public class FormBodyModelBinder : IModelBinder
{
readonly IModelBinder _overriddenModelBinder;
public FormBodyModelBinder(IModelBinder overriddenModelBinder)
{
_overriddenModelBinder = overriddenModelBinder;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var httpContext = bindingContext.HttpContext;
var contentType = httpContext.Request.ContentType;
var hasFormData = httpContext.Request.HasFormContentType;
var hasJsonData = contentType?.Contains("application/json") ?? false;
var bindFromBothBodyAndForm = bindingContext.ModelMetadata is DefaultModelMetadata mmd &&
(mmd.Attributes.Attributes?.Any(e => e is FromBodyAttribute) ?? false) &&
(mmd.Attributes.Attributes?.Any(e => e is FromFormAttribute) ?? false);
if (hasFormData || !hasJsonData || !bindFromBothBodyAndForm)
{
await _overriddenModelBinder.BindModelAsync(bindingContext);
}
else //try binding model from the request body (deserialize)
{
try
{
//save the request body in HttpContext.Items to support binding multiple times
//for multiple arguments
const string BODY_KEY = "___request_body";
if (!httpContext.Items.TryGetValue(BODY_KEY, out var body) || !(body is string jsonPayload))
{
using (var streamReader = new StreamReader(httpContext.Request.Body))
{
jsonPayload = await streamReader.ReadToEndAsync();
}
httpContext.Items[BODY_KEY] = jsonPayload;
}
bindingContext.Result = ModelBindingResult.Success(JsonSerializer.Deserialize(jsonPayload, bindingContext.ModelType));
}
catch
{
bindingContext.Result = ModelBindingResult.Success(Activator.CreateInstance(bindingContext.ModelType));
}
}
}
}
//the corresponding model binder provider
public class FormBodyModelBinderProvider : IModelBinderProvider
{
readonly IModelBinderProvider _overriddenModelBinderProvider;
public FormBodyModelBinderProvider(IModelBinderProvider overriddenModelBinderProvider)
{
_overriddenModelBinderProvider = overriddenModelBinderProvider;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
var modelBinder = _overriddenModelBinderProvider.GetBinder(context);
if (modelBinder == null) return null;
return new FormBodyModelBinder(modelBinder);
}
}
We write a simple extension method on MvcOptions to help configure it to override an IModelBinderProvider with the custom one to support multisource model binding.
public static class MvcOptionsExtensions
{
public static void UseFormBodyModelBinderProviderInsteadOf<T>(this MvcOptions mvcOptions) where T : IModelBinderProvider
{
var replacedModelBinderProvider = mvcOptions.ModelBinderProviders.OfType<T>().FirstOrDefault();
if (replacedModelBinderProvider != null)
{
var customProvider = new FormBodyModelBinderProvider(replacedModelBinderProvider);
mvcOptions.ModelBinderProviders.Remove(replacedModelBinderProvider);
mvcOptions.ModelBinderProviders.Add(customProvider);
}
}
}
To support binding complex model from either form or request body, we can override the ComplexTypeModelBinderProvider like this:
//in the ConfigureServices method
services.AddMvc(o => {
o.UseFormBodyModelBinderProviderInsteadOf<ComplexTypeModelBinderProvider>();
};
That should suit most of the cases in which your action's argument is of a complex type.
Note that the code in the FormBodyModelBinder requires the action arguments to be decorated with both FromFormAttribute and FromBodyAttribute. You can read the code and see where they're fit in. So you can write your own attribute to use instead. I prefer to using existing classes. However in this case, there is an important note about the order of FromFormAttribute and FromBodyAttribute. The FromFormAttribute should be placed before FromBodyAttribute. Per my test, looks like the ASP.NET Core model binding takes the first attribute as effective (seemingly ignores the others), so if FromBodyAttribute is placed first, it will take effect and may prevent all the model binders (including our custom one) from running when the content-type is not supported (returning 415 response).
The final note about this solution, it's not perfect. Once you accept to bind model from multi-sources like this, it will not handle the case of not supporting media type nicely as when using FromBodyAttribute explicitly. Because when we support multi-source model binding, the FromBodyAttribute is not used and the default check is not kicked in. We must implement that ourselves. Because of multiple binders joining in the binding process, we cannot easily decide when the request becomes not supported (so even you add a check in the FormBodyModelBinder, you can successfully set the response's status code to 415 but it may not be right and the request is still being processed in the selected action method). With this note, in case of media type not supported, you should ensure that your code in the action method is not broken by handling empty (or null) model argument.
Here is how you use the custom model binder:
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Register([FromForm] [FromBody] RegisterViewModel model, string returnUrl = null) { }
What I have done is created a small API in a class library. This API would be used by other sites. Think of it as a standard endpoint that all of our websites will contain.
[Route("api/[controller]")]
[ApiController]
public class CustomController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
}
The above is in a class library. Now what i would like to do is be able to add this to the projects in a simple manner.
app.UseCustomAPI("/api/crap");
I am not exactly sure how i should handle routing to the api controllers in the library. I created a CustomAPIMiddleware which is able to catch that i called "/api/crap" however i am not sure how i should forward the request over to CustomController in the library
public async Task Invoke(HttpContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
PathString matched;
PathString remaining;
if (context.Request.Path.StartsWithSegments(_options.PathMatch, out matched, out remaining))
{
PathString path = context.Request.Path;
PathString pathBase = context.Request.PathBase;
context.Request.PathBase = pathBase.Add(matched);
context.Request.Path = remaining;
try
{
await this._options.Branch(context);
}
finally
{
context.Request.PathBase = pathBase;
context.Request.Path = path;
}
path = new PathString();
pathBase = new PathString();
}
else
await this._next(context);
}
After having done that i am starting to think i may have approached this in the wrong manner and should actually be trying to add it directly to the routing tables somehow. That being said i would like it if they could customize the endpoint that the custom controller reads from.
Update
The following does work. Loading and registering API Controllers From Class Library in ASP.NET core
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddApplicationPart(Assembly.Load(new AssemblyName("WebAPI")));
However i am really looking for a middlewere type solution so that users can simply add it and i can configure the default settings or they can change some of the settings. The above example would not allow for altering the settings.
app.UseCustomAPI("/api/crap");
Update from comment without Assembly
If i dont add the .AddApplicationPart(Assembly.Load(new AssemblyName("WebAPI")));
This localhost page can’t be found No webpage was found for the web address:
https://localhost:44368/api/Custom
To customise the routing for a controller at runtime, you can use an Application Model Convention. This can be achieved with a custom implementation of IControllerModelConvention:
public class CustomControllerConvention : IControllerModelConvention
{
private readonly string newEndpoint;
public CustomControllerConvention(string newEndpoint)
{
this.newEndpoint = newEndpoint;
}
public void Apply(ControllerModel controllerModel)
{
if (controllerModel.ControllerType.AsType() != typeof(CustomController))
return;
foreach (var selectorModel in controllerModel.Selectors)
selectorModel.AttributeRouteModel.Template = newEndpoint;
}
}
This example just replaces the existing template (api/[controller]) with whatever is provided in the CustomControllerConvention constructor. The next step is to register this new convention, which can be done via the call to AddMvc. Here's an example of how that works:
services.AddMvc(o =>
{
o.Conventions.Add(new CustomControllerConvention("api/whatever"));
});
That's all that's needed to make things work here, but as you're offering this up from another assembly, I'd suggest an extension method based approach. Here's an example of that:
public static class MvcBuilderExtensions
{
public static IMvcBuilder SetCustomControllerRoute(
this IMvcBuilder mvcBuilder, string newEndpoint)
{
return mvcBuilder.AddMvcOptions(o =>
{
o.Conventions.Add(new CustomControllerConvention(newEndpoint));
});
}
}
Here's how that would be called:
services.AddMvc()
.SetCustomControllerRoute("api/whatever");
This whole approach means that without a call to SetCustomControllerRoute, api/Custom will still be used as a default.
For this current project I am working on, we need to implement a web api. It needs to live inside the existing webforms project. And the specifications say we need to use Owin.
So after wiring everything up using: Microsoft.Owin, Microsoft.Owin.Host.SystemWeb, Microsoft.Owin.Hosting, Microsoft.Owin.Security
A proper startup class with the OwinStartupAttribute.
app.UseWebApi with a windsor IOC container.
Web api seems to work as expected.
Except for the fact that all requests made to the existing website also go through to webapi.
A bit more explanation.
We needed a LanguageMessageHandler : DelegatingHandler. After setting that class up we've started noticing that the breakpoint on 'SendAsync gets caught even when we are not requesting anything webApi related.
The older website shouldn't even have knowledge about this handler.
A bit code the clarify:
The startupclass:
[assembly: OwinStartupAttribute(typeof(Startup))]
public class Startup
{
public void Configuration(IAppBuilder app)
{
var container = ((IContainerAccessor)HttpContext.Current.ApplicationInstance).Container;
app.UseWebApi(container);
}
}
The UseWebApi extension:
public static void UseWebApi(this IAppBuilder app, IWindsorContainer container)
{
var config = new HttpConfiguration
{
DependencyResolver = new WindsorDependencyResolver(container)
};
//Web API Routes
config.MapHttpAttributeRoutes();
//Default to json when requested by browser
config.Formatters.JsonFormatter.MediaTypeMappings.Add(new RequestHeaderMapping("Accept", "text/html", StringComparison.InvariantCultureIgnoreCase, true, "application/json"));
//Add language handler
config.MessageHandlers.Add(new LanguageMessageHandler());
//Ensure initialized
config.EnsureInitialized();
//Start WebApi
app.UseWebApi(config);
}
So now we are trying to figure out why all the requests are handled by the LanguageMessageHandler and not just the requests that are made for webApi.
An example route:
[RoutePrefix("api/dossier")]
public class AdministrationsController : ApiController
{
//GET
[Route("{idtype}_{id}/administrations/planned/")] //?limit={maxdate}&nursingunit={nuid}
[HttpGet]
public IHttpActionResult Planned(string idtype, int id, [FromUri] int maxdate = 6, [FromUri] int? nuid = null)
{
return Ok();
}
}
Fixed by using a filter instead of a message handler.
Was wrongfully asuming that message handler was going to be executed after routing in the pipeline.
I have an ASP.NET Web API app that I have versioned using namespace versioning.
For example, this method is accessible at api/v1/Location/Process:
namespace Project.Controllers.V1
{
public class LocationController : BaseController
{
[HttpsRequired, HttpGet]
public async Task<ApiResponse> Process(string sessionToken, string id)
Swagger is set up to deal with this setup using the MultipleApiVersions method:
c.MultipleApiVersions((apiDesc, version) => {
var path = apiDesc.RelativePath.Split('/');
var pathVersion = path[1];
return CultureInfo.InvariantCulture.CompareInfo.IndexOf(pathVersion, version, CompareOptions.IgnoreCase) >= 0;
},
vc => {
vc.Version("v1", "Foundbite Client API V1");
});
However when the Swagger UI is generated the methods have the full namespace and controller name in the url: api/V1.Location/Process
Anyone know how I can edit the paths Swagger will use so I can create the correct url? (Basically just need to replace that fullstop). I ideally don't want to use routing attributes.
(There is a similar question to mine here but it misses some keen points that I'd like to address.)
I have an application with an ASP .Net MVC 5 front end and a Web Api 2 service layer and I would like to use dependency injection so the MVC 5 controllers only rely on abstractions for the Web Api 2 ones.
Since the Web Api controllers mostly use this kind of signature:
public IHttpActionResult SomeMethod(){ return Ok(); }
my first thought was that the interface should be:
IHttpActionResult SomeMethod();
Now I have a class library with the interfaces for the services but that means that this class library would need a reference to System.Web.Http in order to use the IHttpActionResult interface in the signatures.
I have two questions:
Fist this feels out right wrong that this library has a reference to System.Web.Http, is there an alternative to this?
If there isn't an alternative, when I try to add the reference I only get an older version of the library which does not have a definition for that interface, where can I get the correct version from?
Thank you.
I would shove the common logic into a common library with 'normal' inputs and outputs. The two transports (MVC and web api) can then call this library
It really depends on what you are trying to achieve - aka how much abstraction you want to introduce.
If you wan't to move all the common business logic into a service for re-use from here, and potentially anywhere then yes you want to get rid of the System.Web.Http references.
Do this by having a clean interface/implimentation that simply return the result of the actions something like this:
public interface ICustomerService
{
BaseResponse DoSomething(BaseRequest request);
}
public abstract class BaseResponse
{
public bool IsSuccess { get; set; }
public IList<string> Errors { get; set; }
}
/*
Note: BaseResponse & BaseRequest, follow the command pattern for passing information you would impliment concrete versions of these.
*/
I then allow the controllers for both Web & Api control how to use this BaseResponse to er...respond.
So maybe create a BaseController, and BaseApiController:
For example:
public abstract class BaseApiController : ApiController
{
protected HttpResponseMessage HandleResponse(BaseResponse response)
{
return
!response.IsSuccess
? Request.CreateErrorResponse(HttpStatusCode.BadRequest, response.Errors )
: Request.CreateResponse(HttpStatusCode.OK, response);
}
}
And:
public abstract class BaseController : Controller
{
protected ActionResult HandleResponse(BaseResponse response, string redirectToAction)
{
if (response.IsSuccess)
return RedirectToAction(redirectToAction);
foreach (var error in response.Errors)
{
ModelState.AddModelError(string.Empty, error);
}
return View();
}
}
Then in WebApi Controller:
public HttpResponseMessage DoAction(string param1)
{
return HandleResponse(_customerService.DoSomething(new DoActionRequest { Param1 = param1 }));
}
And in the Web Controller
public ActionResult DoAction(ViewModel viewModel)
{
var response = _customerService.DoSomething(new DoActionRequest { Param1 = param1 });
return HandleResponse(response, "Success");
}
In this way all busienss logic is tucked away and resusable, and the ApiController and Controllers can respond in their own unique ways.