I'm trying to write very simple multi-platform app (iOS & Android) in visual studio. This app uses web service, uploaded on my web hosting.
This is code that call WebAPI (get & post):
async void post_async(object sender, System.EventArgs e)
{
Console.WriteLine("POST");
try
{
HttpClient httpClient = new HttpClient();
var BaseAddress = "https://mywebsite";
var response = await httpClient.PostAsync(BaseAddress, null);
var message = await response.Content.ReadAsStringAsync();
Console.WriteLine($"RESPONSE: " + message);
}
catch (Exception er)
{
Console.WriteLine($"ERROR: " + er.ToString());
}
}
async void get_async(object sender, System.EventArgs e)
{
try
{
HttpClient httpClient = new HttpClient();
var BaseAddress = "https://mywebsite";
var response = await httpClient.GetAsync(BaseAddress);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"RESPONSE: " + content);
}
}
catch (Exception er)
{
Console.WriteLine($"ERROR: " + er.ToString());
}
}
This is very simple code for Web Api:
[HttpGet]
public ActionResult<string> Get()
{
return "get method ok";
}
[HttpPost]
public ActionResult<string> Post()
{
return "post method ok";
}
Very strange issue because for every void I always obtain "get method ok". So "get" is ok, but I don't understand why I cannot call post method.
I tried to use Postman: same issue.
I'm using this very simple code:
[ActionName("getmet")]
public ActionResult<string> getmet()
{
return "get method ok";
}
[ActionName("postmet")]
public ActionResult<string> postmet()
{
return "post method ok";
}
Now of course I can call https://mywebsite/getmet or postmet and it works using postman.
If I use [HttpPost] for postmet method, on Postman I get "404 not found". Why?
var BaseAddress = "https://mywebsite"; //
URL hits get method by difualt, thats means works as https://mywebsite/Get where as your actual post method URL is https://mywebsite/Post
instead of calling a different method use code like below.
[HttpPost]
[HttpGet]
public ActionResult<string> Get()
{
return "method ok";
}
OR you can use API ROUTE
OR
[AcceptVerbs(HttpVerbs.Get|HttpVerbs.Post)]
Related
I have a simple controller in ASP.NET Core that proxies images from the web and provides a resized version when query parameters are passed.
Some of the source images do not have a Content-Length and Transfer-Encoding: chunked, but...
The base class FileResultExecutorBase does not set the Transfer-Encoding header, even though the ContentLength can be null. My understanding is, that this header is necessary when Content-Length is not provided.
When I set the header manually it is ignored.
My sample code:
class Controller
{
public async Task<IActionResult> Proxy(string url)
{
var response = await httpClient.GetAsync(url);
return new FileCallbackResult("", (stream, context) =>
{
await using (var sourceStream = await response.Content.ReadAsStreamAsync())
{
await sourceStream.CopyToAsync(stream);
}
};
}
}
public delegate Task FileCallback(Stream body, HttpContext httpContext);
public sealed class FileCallbackResult : FileResult
{
public FileCallback Callback { get; }
public FileCallbackResult(string contentType, FileCallback callback)
: base(contentType)
{
Callback = callback;
}
public override Task ExecuteResultAsync(ActionContext context)
{
var executor = context.HttpContext.RequestServices.GetRequiredService<FileCallbackResultExecutor>();
return executor.ExecuteAsync(context, this);
}
}
public sealed class FileCallbackResultExecutor : FileResultExecutorBase
{
public async Task ExecuteAsync(ActionContext context, FileCallbackResult result)
{
var (_, _, serveBody) = SetHeadersAndLog(context, result, result.FileSize, result.FileSize.HasValue);
if (result.FileSize == null)
{
context.HttpContext.Response.Headers[HeaderNames.TransferEncoding] = "chunked";
}
if (serveBody)
{
var bytesRange = new BytesRange(range?.From, range?.To);
await result.Callback(
context.HttpContext.Response.Body,
context.HttpContext);
}
}
}
It works fine locally, but I deploy it to our kubernetes cluster with istio, the requests cannot be served. I guess that istio or another proxy does not like the header combination. I have not found anything in the logs yet.
I want to support all 3 of the following content-types in my controller/action.
application/json
application/x-www-form-urlencoded
multipart/form-data
with this signature i can support both urlencoded and form data, however a JSON payload does not get bound to Message
[HttpPost]
public async Task<IActionResult> PostAsync(Message message)
If i want to bind a JSON payload to Message properly i need to use the FromBody attribute like this:
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody]Message message)
however doing this starts throwing 415 erros for the other 2 content types I'm interested in.
My question is, how can I provide a single API endpoint to my customers and give them the flexibility of sending data in any of these 3 content types.
You should add the different content types supported using ConsumesAttribute then update your action as follows;
[HttpPost]
[Consumes("application/json", "multipart/form-data", "application/x-www-form-urlencoded")]
public async Task<IActionResult> PostAsync([FromForm, FromBody, FromQuery]Message message)
First, you should avoid combining the application/json and multipart/form-data for the same action which will make application unstable.
If you insist on this, you need to implement your own ModelBinder by following steps below:
MyComplexTypeModelBinder
public class MyComplexTypeModelBinder : ComplexTypeModelBinder
{
public MyComplexTypeModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders, ILoggerFactory loggerFactory, bool allowValidatingTopLevelNodes) : base(propertyBinders, loggerFactory, allowValidatingTopLevelNodes)
{
}
protected override Task BindProperty(ModelBindingContext bindingContext)
{
try
{
var result = base.BindProperty(bindingContext);
if (bindingContext.Result.IsModelSet == false)
{
var request = bindingContext.HttpContext.Request;
var body = request.Body;
request.EnableRewind();
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
request.Body.Read(buffer, 0, buffer.Length);
var bodyAsText = Encoding.UTF8.GetString(buffer);
var jobject = JObject.Parse(bodyAsText);
var value = jobject.GetValue(bindingContext.FieldName, StringComparison.InvariantCultureIgnoreCase);
var typeConverter = TypeDescriptor.GetConverter(bindingContext.ModelType);
var model = typeConverter.ConvertFrom(
context: null,
culture: CultureInfo.InvariantCulture,
value: value.ToString());
bindingContext.Result = ModelBindingResult.Success(model);
request.Body.Seek(0, SeekOrigin.Begin);
}
return result;
}
catch (Exception ex)
{
throw;
}
}
}
MyComplexTypeModelBinderProvider
public class MyComplexTypeModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
{
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
for (var i = 0; i < context.Metadata.Properties.Count; i++)
{
var property = context.Metadata.Properties[i];
propertyBinders.Add(property, context.CreateBinder(property));
}
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
return new MyComplexTypeModelBinder(
propertyBinders,
loggerFactory,
mvcOptions.AllowValidatingTopLevelNodes);
}
return null;
}
}
Register MyComplexTypeModelBinderProvider in Startup.cs
services.AddMvc(options => {
options.ModelBinderProviders.Insert(0, new MyComplexTypeModelBinderProvider());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
Actually you can have a single endpoint accepting multiple content-types:
[HttpPost]
[Consumes("application/json")]
public async Task<IActionResult> CommonEndpoint([FromBody] JObject payload)
{
...
}
[HttpPost]
[Consumes("x-www-form-urlencoded")]
public async Task<IActionResult> CommonEndpoint(/* this overload doesn't specify the payload in its signature */)
{
var formFieldsDictionary = HttpContext.Request.Form.Keys
.ToDictionary(k => k, k => HttpContext.Request.Form[k].FirstOrDefault());
var payload = JsonConvert.SerializeObject(formFieldsDictionary);
...
}
Your project, written with Asp.net Core, makes deploying Rest API. However, your customer wanted to communicate with soap. How to make an improvement
SoapCore has already done many things for us to support this situation. We apply step-by-step changes to our Asp.net Core project.
First in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
try
{
services.AddSoapServiceOperationTuner(new MyServiceOperationTuner());
services.Configure<XApiSettings>(options =>
{
options.baseurl = XApiOptions[nameof(XApi.baseurl)];
options.accesstokenroute = XApiOptions[nameof(XApi.accesstokenroute)];
options.secret = XApiOptions[nameof(XApi.secret)];
options.secretkey = XApiOptions[nameof(XApi.secretkey)];
options.grant_type = XApiOptions[nameof(XApi.grant_type)];
options.username = XApiOptions[nameof(XApi.username)];
options.password = XApiOptions[nameof(XApi.password)];
});
services.AddSoapCore();
services.AddSingleton<IRESAdapterService>(new RESAdapterService(
Xcontroller: new XApiController(
services.BuildServiceProvider().GetRequiredService<IOptions<XApi>>(),
_corendonLogger
)));
services.AddSoapExceptionTransformer((ex) => ex.Message);
}
catch (Exception ex)
{
Log.Logger.Error("ConfigureServices Message: " + ex.Message);
}
}
If you want your application to be accessible from the root directory at the address you deploy, you can type path '/' directly or name it as '/ XX'
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, ApplicationContext dbContext)
{
try
{
app.UseSoapEndpoint<IRESAdapterService>(path: "/", binding: new BasicHttpBinding(), SoapSerializer.XmlSerializer);
}
}
In the case of requests that are handled on the server side, data sent as xml will normally be null. We need to make the following improvement in SoapCore so that the server can resolve the request.
public class MyServiceOperationTuner : IServiceOperationTuner
{
public void Tune(HttpContext httpContext, object serviceInstance, SoapCore.OperationDescription operation)
{
RESAdapterService service = serviceInstance as RESAdapterService;
service.SetHttpRequest = httpContext.Request;
}
}
In addition, the interface to meet the incoming requests and services to make redirects to our controller should write as follows
[ServiceContract]
public interface IRESAdapterService
{
[OperationContract]
[XmlSerializerFormat(SupportFaults = true)]
Task<OTA_AirAvailRS> getAvailability([FromBody]HttpRequestMessage req);
[OperationContract]
Task<OTA_AirPriceRS> pricing([FromBody]HttpRequestMessage req);
}
public class RESAdapterService : IRESAdapterService
{
XApiController _controller;
public HttpRequest SetHttpRequest { get; set; }
public RESAdapterService(XApiController XApi)
{
_controller = XApi;
}
public Task<MyRequesterClass> Method1([FromBody]HttpRequestMessage req)
{
return _controller.Method1(SetHttpRequest);
}
public Task<MyDiffRequesterClass> Method2([FromBody]HttpRequestMessage req)
{
return _controller. Method2(SetHttpRequest);
}
}
The controller was catching requests from the Request object, but; now the Request object has to get through the router service for the future of null in this context. Therefore we can implement the code that reads XML as follows
Stream reqBody = Request?.Body;
if (Request == null)
reqBody = (MyRequesterClass as HttpRequest).Body;
Let's come to the client side, write a simple framework console project
Normally we offer wsdl visual studio add references by adding the portion of the proxy can create and walk. (Recommended case) But in my case I decided to post xml with webclient because I use a user certificate and I don't know the type of object to send. Sample use below:
static string xml = " <ReqObj xmlns='http://tempuri.org/'>"
+ "</ ReqObj >";
static string wrapbody = #"<?xml version=""1.0"" encoding=""utf-8""?>
<soap:Envelope xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/""
xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""
xmlns:xsd=""http://www.w3.org/2001/XMLSchema"">
<soap:Body>
#
</soap:Body>
</soap:Envelope>";
public static async Task<string> CreateSoapEnvelope()
{
HttpResponseMessage response = await PostXmlRequest("https://localhostorliveaddress.com");
string content = await response.Content.ReadAsStringAsync();
return content;
}
public static async Task<HttpResponseMessage> PostXmlRequest(string baseUrl)
{
X509Certificate2 clientCert = new X509Certificate2(#"D:\ccer\xxx.pfx", "password");
//X509Certificate2 clientCert = GetClientCertificate();
WebRequestHandler requestHandler = new WebRequestHandler();
requestHandler.ClientCertificates.Add(clientCert);
using (var httpClient = new HttpClient(requestHandler))
{
string wrpXmlContent = wrapbody.Replace("#", xml);
var httpContent = new StringContent(wrpXmlContent, Encoding.UTF8, "text/xml");
httpContent.Headers.Add("SOAPAction", "https://localhostorliveaddress.com/method1");
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml"));
return await httpClient.PostAsync(baseUrl, httpContent);
}
}
Getting Client certificate from My User Personel Store
private static X509Certificate2 GetClientCertificate()
{
X509Store userCaStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
userCaStore.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certificatesInStore = userCaStore.Certificates;
X509Certificate2Collection findResult = certificatesInStore.
Find(X509FindType.FindBySubjectName, "XRootCertificateOnMyUserPersonelStore", true);
X509Certificate2 clientCertificate = null;
if (findResult.Count == 1)
{
clientCertificate = findResult[0];
}
else
{
throw new Exception("Unable to locate the correct client certificate.");
}
return clientCertificate;
}
catch
{
throw;
}
finally
{
userCaStore.Close();
}
}
In .Net Core 2.2. I am creating a API Controller that routes the request to another Http endpoint based on payload.
[Route("api/v1")]
public class RoutesController : Controller
{
[HttpPost]
[Route("routes")]
public async Task<IActionResult> Routes([FromBody]JObject request)
{
var httpClient = new HttpClient();
// here based on request httpCLient will make `POST` or `GET` or `PUT` request
// and returns `Task<HttpResponseMessage>`. Lets assume its making `GET`
// call
Task<HttpResponseMessage> response = await
httpClient.GetAsync(request["resource"]);
/* ??? what is the correct way to return response as `IActionResult`*/
}
}
based on SO post i can do this
return StatusCode((int)response.StatusCode, response);
However i am not sure sending HttpResponseMessage as ObjectResult is correct way.
I also want to make sure content negotiation will work.
Update 7/25/2022
Updated the correct answer
public class HttpResponseMessageResult : IActionResult
{
private readonly HttpResponseMessage _responseMessage;
public HttpResponseMessageResult(HttpResponseMessage responseMessage)
{
_responseMessage = responseMessage; // could add throw if null
}
public async Task ExecuteResultAsync(ActionContext context)
{
var response = context.HttpContext.Response;
if (_responseMessage == null)
{
var message = "Response message cannot be null";
throw new InvalidOperationException(message);
}
using (_responseMessage)
{
response.StatusCode = (int)_responseMessage.StatusCode;
var responseFeature = context.HttpContext.Features.Get<IHttpResponseFeature>();
if (responseFeature != null)
{
responseFeature.ReasonPhrase = _responseMessage.ReasonPhrase;
}
var responseHeaders = _responseMessage.Headers;
// Ignore the Transfer-Encoding header if it is just "chunked".
// We let the host decide about whether the response should be chunked or not.
if (responseHeaders.TransferEncodingChunked == true &&
responseHeaders.TransferEncoding.Count == 1)
{
responseHeaders.TransferEncoding.Clear();
}
foreach (var header in responseHeaders)
{
response.Headers.Append(header.Key, header.Value.ToArray());
}
if (_responseMessage.Content != null)
{
var contentHeaders = _responseMessage.Content.Headers;
// Copy the response content headers only after ensuring they are complete.
// We ask for Content-Length first because HttpContent lazily computes this
// and only afterwards writes the value into the content headers.
var unused = contentHeaders.ContentLength;
foreach (var header in contentHeaders)
{
response.Headers.Append(header.Key, header.Value.ToArray());
}
await _responseMessage.Content.CopyToAsync(response.Body);
}
}
}
You can create a custom IActionResult that will wrap transfere logic.
public async Task<IActionResult> Routes([FromBody]JObject request)
{
var httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.GetAsync("");
// Here we ask the framework to dispose the response object a the end of the user resquest
this.HttpContext.Response.RegisterForDispose(response);
return new HttpResponseMessageResult(response);
}
public class HttpResponseMessageResult : IActionResult
{
private readonly HttpResponseMessage _responseMessage;
public HttpResponseMessageResult(HttpResponseMessage responseMessage)
{
_responseMessage = responseMessage; // could add throw if null
}
public async Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = (int)_responseMessage.StatusCode;
foreach (var header in _responseMessage.Headers)
{
context.HttpContext.Response.Headers.TryAdd(header.Key, new StringValues(header.Value.ToArray()));
}
if(_responseMessage.Content != null)
{
using (var stream = await _responseMessage.Content.ReadAsStreamAsync())
{
await stream.CopyToAsync(context.HttpContext.Response.Body);
await context.HttpContext.Response.Body.FlushAsync();
}
}
}
}
ASP.NET Core has the return object RedirectResult to redirect the caller.
Simply wrap the response in Ok() Action return type:
return Ok(response)
so your code would look something like:
[Route("api/v1")]
public class RoutesController : Controller
{
[HttpPost]
[Route("routes")]
public async Task<IActionResult> Routes([FromBody]JObject request)
{
var httpClient = new HttpClient();
Task<HttpResponseMessage> response = await httpClient.GetAsync(request["resource"]);
return Ok(response);
}
}
More info here: https://learn.microsoft.com/en-us/aspnet/core/web-api/action-return-types?view=aspnetcore-3.1
Using ASP.NET WebAPI 2.0 and have a conceptual issue.
Would like to keep a global record of any API that is called by any user/ client and it would be awesome if this was stored in the database.
What would be the best mechanism to accomplish this?
I' using a DelegatingHandler for a long time in several projects which is doing just fine.
public class ApiCallLogHandler : DelegatingHandler {
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
var started = DateTime.UtcNow;
HttpResponseMessage response = null;
Exception baseException = null;
try {
response = await base.SendAsync(request, cancellationToken);
} catch(Exception exception) {
CommonLogger.Logger.LogError(exception);
baseException = exception;
}
try {
var callModel = await GetCallModelAsync(request, response);
if(baseException != null)
callModel.Exception = baseException
callModel.ExecutionTime = (DateTime.UtcNow - started).ToString();
await CommonLogger.Logger.LogApiCallAsync(callModel);
} catch (Exception exception) {
CommonLogger.Logger.LogError(exception);
}
return response;
}
private async Task<ApiCallModel> GetCallModelAsync(HttpRequestMessage request, HttpResponseMessage response) {
// parse request and response and create a model to store in database...
}
}
By this approach you are able to track all requests, exceptions during execution, and even full-response of each API call.
ApiCallModel is just a simple POCO class which you should fill it with your required data from request and response.
CommonLogger.Logger.* is your logging mechanism.
And, you have to register the handler with this snippet:
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
config.MessageHandlers.Add(new ApiCallLogHandler());
}
}