How can I retrieve the api controller name and api action name inside a piece of custom OWIN middleware? I can do it inside of a message handler like so:
var config = request.GetConfiguration();
var routeData = config.Routes.GetRouteData(request);
var controllerContext = new HttpControllerContext(config, routeData, request);
request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData;
controllerContext.RouteData = routeData;
var controllerDescriptor = new
DefaultHttpControllerSelector(config).SelectController(request);
controllerContext.ControllerDescriptor = controllerDescriptor;
var actionMapping = new ApiControllerActionSelector().SelectAction(controllerContext);
//controller name
controllerDescriptor.ControllerName
//action name
actionMapping.ActionName
Update:
Here is my current piece of OWIN middleware. How can I get the controllerName and actionName within this code?
using AppFunc = Func<IDictionary<string, object>, Task>;
public class LoggingMiddleware
{
private readonly AppFunc _next;
private static readonly ILog RequestApiLogger = LogManager.GetLogger("RequestApiPacketLogger");
private static readonly ILog ResponseApiLogger = LogManager.GetLogger("ResponseApiPacketLogger");
public LoggingMiddleware(AppFunc next)
{
_next = next;
}
public async Task Invoke(IDictionary<string, object> environment)
{
var correlationId = Guid.NewGuid();
IOwinContext context = new OwinContext(environment);
// Buffer the request (body is a string, we can use this to log the request later
var requestBody = new StreamReader(context.Request.Body).ReadToEnd();
var requestData = Encoding.UTF8.GetBytes(requestBody);
context.Request.Body = new MemoryStream(requestData);
// Buffer the response
var responseBuffer = new MemoryStream();
var responseStream = context.Response.Body;
context.Response.Body = responseBuffer;
// add the "http-tracking-id" response header so the user can correlate back to this entry
var responseHeaders = (IDictionary<string, string[]>)environment["owin.ResponseHeaders"];
responseHeaders["http-tracking-id"] = new[] { correlationId.ToString("d") };
IDictionary<string, string[]> responseHeadersClone = new Dictionary<string, string[]>(responseHeaders);
//invoke the next piece of middleware in the pipeline
await _next.Invoke(environment);
// rewind the request and response buffers and record their content
responseBuffer.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(responseBuffer);
var responseBody = await reader.ReadToEndAsync();
// log the request/response as long at it wasn't preflight
if (context.Request.Method.ToUpper() != "OPTIONS")
{
RequestApiLogger.LogHttpRequestAsync(context, correlationId, requestBody);
ResponseApiLogger.LogHttpResponseAsync(context, correlationId, responseBody, responseHeadersClone);
}
// You need to do this so that the response we buffered is flushed out to the client application.
responseBuffer.Seek(0, SeekOrigin.Begin);
await responseBuffer.CopyToAsync(responseStream);
}
}
You really can't. OWIN middleware does not know about Web Api. It only knows about the environment that is passed to it. The idea of middleware is that it is independent of the hosting and application platforms.
You didn't provide a specific example of what you are trying to accomplish, so there might be a way of doing what you are trying to do.
Update to include response to the above statement:
You can reverse what you are trying to do. Kinda. The OWIN environment is available on the HttpRequest inside Web Api with the GetOwinEnvironmentExtension method. You could add an environment variable to the dictionary with the name of the controller and method inside the controller and then use that when your middleware is called after web api is finished. Lots of repetitive code, but it would work.
There is probably a way to intercept the method before it is called. Check out this answer from #mark-jones that might give you some insight to doing that.
Hope that helps.
Related
In my .NET Core 3.1 Web API project, I have a filter that implements IAsyncExceptionFilter. In that filter, I'm catching unhandled exceptions, logging them to database along with the incoming HTTP request data(at least I intend to).
My filter looks like this:
public class ServiceExceptionInterceptor : IAsyncExceptionFilter
{
public async Task OnExceptionAsync(ExceptionContext context)
{
var errorLogService = (IErrorLogService) context.HttpContext.RequestServices.GetService(typeof(IErrorLogService));
var req = context.HttpContext.Request;
var method = req.Method;
var uri = $"{req.Scheme}://{req.Host}/{req.Path}{req.QueryString}";
string body = null;
if (req.ContentLength != null)
{
var reader = new StreamReader(req.Body);
// body is empty string
body = await reader.ReadToEndAsync();
}
var exception = new Models.DB.Exception()
{
Type = context.Exception.GetType().ToString(),
Message = context.Exception.Message,
StackTrace = context.Exception.StackTrace,
CreatedAt = DateTime.UtcNow,
Method = method,
Uri = uri,
Body = body
};
errorLogService.LogError(exception);
var error = new Error()
{
StatusCode = 500,
Message = "Something went wrong. There were some technical issues while processing your request."
};
context.Result = new JsonResult(error) { StatusCode = StatusCodes.Status500InternalServerError };
}
In the code above, I check if there is any request body with req.ContentLength != null, and it seems like it works fine. When I get a request with no body, req.ContentLength is null, and if the request does have a body, it has a value.
But when it comes to getting the content of the request body when there is one, the value I get is always empty string.
As far as I understood from some articles and forum posts, the reason behind this is that the body is read in my controller endpoint, and can not be re-read. I saw a solution that uses the EnableBuffering() method in a middleware in Startup like below, but it did not work for me.
app.Use(async (context, next) => {
context.Request.EnableBuffering();
await next();
});
When I remove the [FromBody] part from my controller, only then I am able to get the request body.
[HttpPost]
[Route("api/questions/{questionId}/comments")]
public async Task<ActionResult<Comment>> AddComment(int questionId, /*[FromBody] AddCommentRequestBody requestBody*/) // -> removing [FromBody] allows the filter to be able to read the request body
{
throw new Exception("test exception");
// ...
// AddComment implementation...
// ...
return Ok(addedComment);
}
Is there any way to read the request body without removing [FromBody] from my controller endpoints?
I am trying to create a TeamCity project from .NET Core. I have written the code below but when I am trying to run the code I get an error.
Code:
public void CreateProject(string name, string newProjectId, string parentProjectId)
{
InvokeTeamCityApi(() => _apiClient.CreateProject(new NewProjectDescription
{
Name = name,
Id = newProjectId,
SourceProject = new GetProjectResponse
{
Id = _toolContext.HostingContext.TeamCityProjectTemplate
},
ParentProject = new GetProjectResponse
{
Id = parentProjectId
},
CopyAllAssociatedSettings = true,
}));
}
Error:
403 Forbidden: CSRF Header X-TC-CSRF-Token does not match CSRF session value
Also I did a Google search and I tried adding the header origin but I dont have access to disable the internal teamcity properties to disable CSRF check.
So I am passing the token X-tc-CSRF-Token in the request but it says the value doesn't match. How can I solve this issue?
Note: I am getting this issue only when I am using bearer token and with basic auth it works fine.
I recently had the same issue when upgrading from TeamCity 10 to TeamCity 2022 and eventually managed to find out how to fix it. Importantly, I didn't have to have access the internal TeamCity properties to disable the CSRF check as I figured that it's not a bad thing to have the extra layer of security.
Things that were probably important that I changed:
I generated an access token for the account that I was using to access the REST API so I wouldn't need to put my password in the code. The access token can be associated with more restrictive permissions than your own account.
Using HttpClient, authorisation is achieved by adding a default header (via the DefaultHeaders property) with the name "Authorization" and value "Bearer {apiToken}".
I read the /authenticationTest.html?csrf endpoint to get a CSRF token then add that value as a default header ("X-TC-CSRF-Token"). It's also possible to use the "tc-csrf-token" header in the same way.
Note that because the "Authorization" and "X-TC-CSRF-Token"/"tc-csrf-token" headers are added as default headers, there's no need to do this explicitly on every HTTP (GET, POST ,DELETE, etc.) method.
Here's some sample code which covers all of the above.
public class Sample
{
private readonly string _baseUrl;
private readonly HttpClient _httpClient = new HttpClient();
public Sample(string baseUrl, string accessToken)
{
_baseUrl = baseUrl;
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken}");
var csrfToken = get("/authenticationTest.html?csrf");
_httpClient.DefaultRequestHeaders.Add("X-TC-CSRF-Token", csrfToken);
}
public void CreateProject(string id, string name)
{
post("/app/rest/projects", $"<newProjectDescription id=\"{id}\" name=\"{name}\" />");
}
private string get(string relativeUrl)
{
var getTask = _httpClient.GetAsync(_baseUrl + relativeUrl);
getTask.Wait();
var response = getTask.Result;
response.EnsureSuccessStatusCode();
var readTask = response.Content.ReadAsStreamAsync();
readTask.Wait();
using ( var stream = readTask.Result )
using ( var reader = new StreamReader(stream) )
return reader.ReadToEnd();
}
private void post(string relativeUrl, string data)
{
using ( var content = new StringContent(data, Encoding.UTF8, "application/xml") )
{
var postTask = _httpClient.PostAsync(_baseUrl + relativeUrl, content);
postTask.Wait();
var response = postTask.Result;
response.EnsureSuccessStatusCode();
var readTask = response.Content.ReadAsStreamAsync();
readTask.Wait();
}
}
}
Call it using, e.g.
// Change these according to your situation
var baseUrl = "http://localhost:8111";
var accessToken = "1234567890";
var sample = new Sample(baseUrl, accessToken);
sample.CreateProject("ProjectID", "ProjectName");
Note that this is just a sample to get you on your feet: I've kept the code short by taking various shortcuts in the code (e.g. the HttpClient should really be one static HttpClient for the application: see Guidelines for using HttpClient on learn.microsoft.com).
I'm writing a simple dotnet core API, under search controller which like below :
[HttpGet("order")]
public async Task <Order> SearchOrder(string ordername, int siteid) {
return await service.getorder(ordername,siteid)
}
The swagger UI where the path https://devehost/search/order test pretty work, but when I use another client to call this api by below
client = new HttpClient {
BaseAddress = new Uri("https://devehost")
};
var request = new HttpRequestMessage(HttpMethod.Get, "Search/order") {
Content = new FormUrlEncodedContent(
new List<KeyValuePair<string, string>> {
new("ordername", "pizza-1"),
new("siteid", "1"),
})
};
var response = await client.SendAsync(request);
The status code always return bad request. But the postman is work, can I know the problem inside?
Thank you
For a GET request, the parameters should be sent in the querystring, not the request body.
GET - HTTP | MDN
Note: Sending body/payload in a GET request may cause some existing implementations to reject the request — while not prohibited by the specification, the semantics are undefined.
For .NET Core, you can use the Microsoft.AspNetCore.WebUtilities.QueryHelpers class to append the parameters to the URL:
Dictionary<string, string> parameters = new()
{
["ordername"] = "pizza-1",
["siteid"] = "1",
};
string url = QueryHelpers.AppendQueryString("Search/order", parameters);
using var request = new HttpRequestMessage(HttpMethod.Get, url);
using var response = await client.SendAsync(request);
I'm designing microservice architecture as below:
Gateway uses Ocelot to forward requests. I would like to change the body in request received from mobile device on gateway side and add inside the body new GUID. Microservices uses CQRS pattern, so command shouldn't returns anything. I implemented custom middleware to change DownstreamContext:
public override async Task Execute(DownstreamContext context)
{
var secondRequest = JObject.Parse(await context.DownstreamRequest.Content.ReadAsStringAsync());
secondRequest["token"] = "test";
secondRequest["newId"] = Guid.NewGuid();
context.DownstreamRequest.Content = new StringContent(secondRequest.ToString(), Encoding.UTF8);
await this.Next(context);
}
I debugged this and content of DownstreamRequest before call await this.Next(context); is changed, but request incoming to microservice is not changed. Is there any way to change request in gateway and forward this request to microservice in a changed form?
You can use for it a custom middleware
public class SetGuidMiddleware
{
private readonly RequestDelegate _next
public SetGuidMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (!HttpMethods.IsGet(context.Request.Method)
&& !HttpMethods.IsHead(context.Request.Method)
&& !HttpMethods.IsDelete(context.Request.Method)
&& !HttpMethods.IsTrace(context.Request.Method)
&& context.Request.ContentLength > 0)
{
//This line allows us to set the reader for the request back at the beginning of its stream.
context.Request.EnableRewind();
var buffer = new byte[Convert.ToInt32(context.Request.ContentLength)];
await context.Request.Body.ReadAsync(buffer, 0, buffer.Length);
var bodyAsText = Encoding.UTF8.GetString(buffer);
var secondRequest = JObject.Parse(bodyAsText);
secondRequest["token"] = "test";
secondRequest["newId"] = Guid.NewGuid();
var requestContent = new StringContent(secondRequest.ToString(), Encoding.UTF8, "application/json");
context.Request.Body = await requestContent.ReadAsStreamAsync();
}
await _next(context);
}
}
and use it before Ocelot
app.UseMiddleware<SetGuidMiddleware>();
app.UseOcelot().Wait();
HeyGuys
I'm working on a WebApi project that receives requests from clients and redirects these requests to other services that are not open for direct access.
By default, .Net serializes and deserializes the Json request parameters automatically, so I need to re-serialize them before calling the appropriate service. The same problem occurs when receiving the service response. I need to deserialize it before sending the response to the user; otherwise .Net framework will serialize it one more time, resulting in a "Json of Json" response.
I found this answer but it does not seem to work with .NetCore; so I tried to create my own ModelBinder that just reads the Json object and returns it.
class JsonUnformatterBinderProvider : Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
return new JsonUnformatterBinder(new SimpleTypeModelBinder(context.Metadata.ModelType));
}
}
and
class JsonUnformatterBinder : IModelBinder
{
private readonly IModelBinder _fallbackBinder;
public JsonUnformatterBinder(IModelBinder fallbackBinder)
{
_fallbackBinder = fallbackBinder;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
string currMethod = bindingContext.ActionContext.HttpContext.Request.Method;
if ("POST".Equals(currMethod) || "PUT".Equals(currMethod))
{
string strData = new StreamReader(bindingContext.ActionContext.HttpContext.Request.Body).ReadToEnd();
bindingContext.Result = ModelBindingResult.Success(strData);
return Task.CompletedTask;
}
return _fallbackBinder.BindModelAsync(bindingContext);
}
}
This code is very simple, it was my first attempt and it worked well for my purposes. However, I still get the "Json of Json" problem when I take the second service answer and returns back to the user.
I basically have no idea what I can do to overcome this, so any workaround is welcome here.
If you need just redirect a request without modification, you could read it from input stream directly and send it to inner service. You could also use such approach to read responce from inner service.
//1. Set empty parameter list in action then neither serializator nor model binder are not invoked.
public async Task<ContentResult> ProxyAction(/*empty parameter list*/)
{
var newUrl = #"https://stackoverflow.com";
var data = this.Request.Body;
using (var client = new HttpClient())
{
//2. Read request body from input stream.
var reader = new StreamReader(data);
var json = reader.ReadToEnd();
using (var content = new StringContent(json))
{
//3. Set correct content type
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(this.Request.ContentType);
//4. Post request to inner service
var response = await client.PostAsync(newUrl, content);
//5. Read response without deserialization
var innerResponse = await response.Content.ReadAsStringAsync();
var contentType = response.Content.Headers.ContentType.ToString();
var statusCode = response.StatusCode;
//6. Return inner response without serialization
var outerResponse = this.Content(innerResponse, contentType);
outerResponse.StatusCode = (int)statusCode;
return outerResponse;
}
}
}