I'm using OData with ASP.NET Core to work with Oracle database. I have a Startup.cs file where I configure authentication, build the model and load all dependencies. I am also reading a config file to load custom odata controllers identified like this :
CustomControllerList": [
{
"Assembly": "assemblyName",
"PublicToken" : "123456789",
"Controller": "UsersController",
"Name": "Users",
"Entity": "ModelNameSpace.UserEntity",
"AccessRights" : [1, 2, 3]
},
{
"Assembly": "assemblyName",
"PublicToken" : "123456789",
"Controller": "GroupsController",
"Name": "Users",
"Entity": "ModelNameSpace.GroupsEntity",
"AccessRights" : [4, 5, 6]
}]
I would like to store access rights related to a controller in order to check before each request if user is authorized.
Concerning code execution before each request, I saw that creating a new controller's functions attribute like this
public class AccessRightsFilterAttribute : Attribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
try
{
String username = Tools.GetUsernameFromToken(context.Controller as ControllerBase);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
My first question is : Is this the only one solution for executing code before each request ? Is there a way to avoid having to put attribute for every OData controller's functions ?
I can't connect to db inside Startup execution and custom OData controllers are not able to access the Startup project.
So my second question is : What is the best practice for reading this data from OData controllers ? I didn't found anything, my only choice is to write inside a file but I would like to know if there is a better way to do it.
Thanks in advance !
Related
I have implemented Audit Trail in asp.net core 3.1 application using great library which has a very good documentation also : https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.WebApi/README.md
I have implemented it in a asp.net core 3.1 web api project with the recommended approach : Middleware + Action Filters (Asp.Net Core): Adding the Audit Middleware together with the Global Action Filter (or Local Action Filters).
I have the following sample output:
{
"EventType":"POST Values/Post",
"Environment":{
"UserName":"Federico",
"MachineName":"HP",
"DomainName":"HP",
"CallingMethodName":"WebApiTest.Controllers.ValuesController.Post()",
"AssemblyName":"WebApiTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"Culture":"en-US"
},
"StartDate":"2017-03-09T18:03:05.5287603-06:00",
"EndDate":"2017-03-09T18:03:05.5307604-06:00",
"Duration":2,
"Action":{
"TraceId": "0HLFLQP4HGFAF_00000001",
"HttpMethod":"POST",
"ControllerName":"Values",
"ActionName":"Post",
"ActionParameters":{
"value":{
"Id":100,
"Text":"Test"
}
},
"FormVariables":{
},
"RequestUrl":"http://localhost:65080/api/values",
"IpAddress":"127.0.0.1",
"ResponseStatus":"OK",
"ResponseStatusCode":200,
"RequestBody":{
"Type":"application/json",
"Length":27,
"Value":"{ Id: 100, Text: \"Test\" }"
},
"ResponseBody":{
"Type":"SomeObject",
"Value":{
"Id":1795824380,
"Text":"Test"
}
},
"Headers": {
"Connection": "Keep-Alive",
"Accept": "text/html, application/xhtml+xml, image/jxr, */*",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-GB",
"Host": "localhost:37341",
"User-Agent": "Mozilla/5.0, (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0), like, Gecko"
}
}
}
From the above output I want ActionParameters not to be captured as part of the Audit trail data. I have gone through the documentation but did not see any out of the box solution for it.
Can anyone help me here with some code sample which will serve as a reference for my implementation
There are at least three ways to accomplish this.
You can avoid the parameters being captured in the event object by marking those with [AuditIgnore] Attribute in the action method.
[HttpPost]
public IEnumerable<string> PostAccount(string user, [AuditIgnore]string password)
{
// password argument will not be audited
}
Or you can remove the action parameters from the event object before saving the scope, by using a custom action:
// On your start-up code
using Audit.WebApi;
Audit.Core.Configuration.AddCustomAction(ActionType.OnEventSaving, scope =>
{
scope.GetWebApiAuditAction().ActionParameters = null;
});
Or, as ChatGPT suggested, you could implement your own AuditDataProvider, inheriting from the Data Provider you currently use, and removing the Action Parameters before calling the real provider's InsertEvent/ReplaceEvent methods. But this complicates things unnecessarily.
public class CustomAuditDataProvider : Audit.Core.Providers.FileDataProvider
{
public override object InsertEvent(AuditEvent auditEvent)
{
RemoveActionParams(auditEvent);
return base.InsertEvent(auditEvent);
}
public override Task<object> InsertEventAsync(AuditEvent auditEvent)
{
RemoveActionParams(auditEvent);
return base.InsertEventAsync(auditEvent);
}
public override void ReplaceEvent(object path, AuditEvent auditEvent)
{
RemoveActionParams(auditEvent);
base.ReplaceEvent(path, auditEvent);
}
public override Task ReplaceEventAsync(object path, AuditEvent auditEvent)
{
RemoveActionParams(auditEvent);
return base.ReplaceEventAsync(path, auditEvent);
}
private void RemoveActionParams(AuditEvent auditEvent)
{
auditEvent.GetWebApiAuditAction().ActionParameters = null;
}
}
// In your start-up code:
Audit.Core.Configuration.Setup().UseCustomProvider(new CustomAuditDataProvider());
I'm using NET5 webapi with HotChocolate package and am trying to inject a service.
I've followed both standard and method based approach documented here however it doesn't work at all. All i get is the following message:
{
"errors": [
{
"message": "Unexpected Execution Error",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"tests"
]
}
],
"data": {
"tests": null
}
}
My query:
query{
tests {
id
}
}
My code currently reflects the method approach as documented.
In startup:
services
.AddSingleton<ITestService, TestService>()
.AddGraphQLServer()
.AddDefaultTransactionScopeHandler()
.AddQueryType<Queries>()
.AddMutationType<Mutations>()
.AddFiltering()
.AddSorting()
.AddProjections()
.AddType<Test>();
The query setup:
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<Test> GetTests([Service] ITestService testService) => testService.GetTests();
My TestService:
private readonly IDbContextFactory<TestDbContext> contextFactory;
public TestService(IDbContextFactory<TestDbContext> contextFactory)
{
this.contextFactory = contextFactory;
}
public IQueryable<Test> GetTests()
{
using var context = contextFactory.CreateDbContext();
return context.Test;
}
I'm sure I'm missing something simple to make this work.
Your DB Context is disposed. The using in GetTests is scoped for this method. On the end of this method the db context gets disposed and a Queryable is returned. Once the execution engine executes the Queryable, the exception is thrown
Checkout the EF Core integration of HotChocolate:
https://chillicream.com/docs/hotchocolate/integrations/entity-framework
I'm learning APB framework. ABP can automagically configure the application services as API Controllers by convention. The documentation says it is possible to fully customize it.
In the example they provide, I would like to change an action name of one of the following endpoints:
/api/app/book to /api/app/books.
But unfortunately I cannot find how to do it.
I tried to change the ActionName of the corresponding service method:
public class BookAppService :
CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto,
CreateUpdateBookDto, CreateUpdateBookDto>,
IBookAppService
{
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
}
[ActionName("books"), HttpGet]
public override Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
return base.GetListAsync(input);
}
}
But the resulting endpoint is not what I want:
Any idea how to do it ?
in abp.io (ABP VNext) , you can diable or configure ConventionalControllers in HttpModules config file of your Project .
Edit this Method :
private void ConfigureConventionalControllers()
{
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers.Create(typeof(SystemApplicationModule).Assembly);
});
}
For all those stumbling accross logging issues I created a small public playground on Github. Feel free to join.
I am still experimenting with the one hits all logging filter for my requests.
The goal is simple. Instead of writing the logs in each controller method implement them once in a filter (or middleware) and apply them globally to all. This is used when something wents wrong I can see which data was used causing the problem.
so for example a controller action (with the globally applied filter)
[HttpPut("{id:int}")
public IActionResult UpdateModel(int id, [FromBody] MyRequestModel request) {}
would create logs like
[Timestamp] INFO: MyNamespace.Controllers.MyModelController.UpdateModel
{
"Parameters": {
"Id": 1,
"Request": {
"Name": "abc",
"SomeInt": 3
}
}
}
Pretty, no more logging manually in each method.
But wait: it's an API and I used the [ApiController] attribute and the request model has invalid data (let's say Name = "a" but it needs to be at least of length of 3).
This gives me 3 problems
ActionFilter.OnActionExecuting is shortcutted and does not get called (due to the ApiController)
the binding of the arguments seems to be skipped and the bound (invalid) data is not applied to the ActionArguments
only the ResultFilter.OnResultExecuted is called but there seems to be no way for accessing/logging the invalid data
This somehow means logging works only when everything goes well but aren't the most interesting logs those were things go wrong?
public void OnActionExecuting(ActionExecutingContext context)
{
_loggerFactory
.CreateLogger(context.ActionDescriptor.DisplayName.Split(" ")[0])
.LogInformation
(
JsonConvert.SerializeObject
(
new { Parameters = context.ActionArguments },
Formatting.Indented
)
);
}
Point 1: I of course could remove the ApiController from each controller and go back to return BadRequest results manually. But I like the centralized approach and would like to stick with it.
I liked the model binding approach giving me classes to serialize for the logs (instead of reading the request body manually as one string). With a custom Json contract resolver I am able to mark model properties as sensitive and they are hidden in the logs (for those how care about security).
So my actual question:
Is there a way to get the model binding values in a ResultFilter or are they thrown away totally?
Or is there a way to hook into the model binding/model validation and write the logs there before they get thrown away?
Both especially for the case where the ApiController attribute starts shortcutting the filter pipeline.
public class LogFilter : IActionFilter, IResultFilter, IExceptionFilter
{
private readonly ILoggerFactory _loggerFactory;
public LogFilter(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
}
public void OnActionExecuting(ActionExecutingContext context)
{
_loggerFactory
.CreateLogger(context.ActionDescriptor.DisplayName.Split(" ")[0])
.LogInformation
(
JsonConvert.SerializeObject
(
new
{
RequestBody = ReadBodyAsString(context.HttpContext.Request),
Parameter = context.ActionArguments
},
Formatting.Indented
)
);
}
public void OnActionExecuted(ActionExecutedContext context) {}
public void OnResultExecuting(ResultExecutingContext context)
{
if (!context.ModelState.IsValid)
{
_loggerFactory
.CreateLogger(context.ActionDescriptor.DisplayName.Split(" ")[0])
.LogWarning
(
JsonConvert.SerializeObject
(
new
{
RequestBody = ReadBodyAsString(context.HttpContext.Request),
ModelErrors = context.ModelState
.Where(kvp => kvp.Value.Errors.Count > 0)
.ToDictionary
(
kvp => kvp.Key,
kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()
)
},
Formatting.Indented
)
);
}
}
public void OnResultExecuted(ResultExecutedContext context) {}
public void OnException(ExceptionContext context)
{
_loggerFactory
.CreateLogger(context.ActionDescriptor.DisplayName.Split(" ")[0])
.LogError
(
context.Exception,
JsonConvert.SerializeObject
(
new
{
//RequestBody = ReadBodyAsString(context.HttpContext.Request) // body is disposed here
},
Formatting.Indented
)
);
}
}
You can create a class which implements OnActionExecuting method of IActionFilter. And register it in global filter so that it applies to all controllers and actions. When exception happens for model binding(coz of length), your OnActionExecuting method still gets called even when [ApiController] is used and you can log it there.
Eg.
public class MyActionFilterAttribute: IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
//Your logic to log the request, you can get the details from
//context parameter
//You can check if model state is valid also by using the property
//context.ModelState.IsValid
}
}
In Startup.cs, you need to set SuppressModelStateInvalidFilter to true. This will not return the 400 status automatically. Your controller method still gets called and since you have action filter, the OnActionExecuting gets called.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(
config =>
{
config.Filters.Add(new MyActionFilterAttribute());
})
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
}
I think you are right about the ActionFilter only catch successful requests.
For your invalid requests, maybe you can have a look at the InvalidModelStateResponseFactory.
When using options.InvalidModelStateResponseFactory you can read your Model and validationerrors.
In previous asp.net web api, I implement DefaultHttpControllerSelector to specify how I want the request to locate my controller. I often have different controllers with different names but intended for same processes. The only difference is that one is of higher version than the other.
For example, I could have a controller named BookingV1Controller, which would be meant to handle the version one of the service. I would also have BookingV2Controller, which was designed to handle the version two of the service. A client application would then make a request to the service with this url http://myservice.com/api/v2/booking/someaction?id=12. To handle the request, I would provide a custom implementation of DefaultHttpControllerSelector to select the appropriate version of the controller required based on the requested version.
However, I seems not to have a way to do this in ASP.NET Core. I have searched everywhere to no avail. No documentation that could help either.
I would appreciate if anyone can be of help to me here. Thanks.
UPDATE
I would also like to know what to do if the version is specified in a custom header. E.g X-Version:v1
UPDATE 2
The requirement was that the version of the service should not be exposed in the URL. If no version is present, the service returns with instruction on how to add the version. If a requested controller is not present in the version requested, the system searches through the lower versions. If it finds it in any lower versions, it uses that. The reason for this is to prevent repetition of controllers on all versions. But with ASP.NET Core, this might not be possible.
This is a very old question that I stumbled upon, but there are much better solutions now. There is this package
Microsoft.AspNetCore.Mvc.Versioning
Which has a much more feature rich way of implementing versioning controls. These include being able to use URL query strings, url paths, headers, or custom version readers. Being able to read the version from HTTPContext etc.
In short, you add the following into your ConfigureServices method in startup.cs
services.AddApiVersioning(o => {
o.ReportApiVersions = true;
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
});
Then you have to decorate your controllers with an ApiVersion.
[ApiVersion("1.0")]
[Route("api/home")]
public class HomeV1Controller : Controller
{
[HttpGet]
public string Get() => "Version 1";
}
[ApiVersion("2.0")]
[Route("api/home")]
public class HomeV2Controller : Controller
{
[HttpGet]
public string Get() => "Version 2";
}
You can also implement it in the path by putting it in the route.
[ApiVersion("1.0")]
[Route("api/{version:apiVersion}/home")]
public class HomeV1Controller : Controller
{
[HttpGet]
public string Get() => "Version 1";
}
[ApiVersion("2.0")]
[Route("api/{version:apiVersion}/home")]
public class HomeV2Controller : Controller
{
[HttpGet]
public string Get() => "Version 2";
}
When you go down this method of actually having it implemented via the Microsoft package, it also means that you are able to deprecate versions, have version discovery, access the version number from the HttpContext easily etc. None of which you could really do if it's just hardcoded in your route.
For more info (Including using it in a header) :
http://dotnetcoretutorials.com/2017/01/17/api-versioning-asp-net-core/
http://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEasy.aspx
https://github.com/Microsoft/aspnet-api-versioning/wiki
I created a package for this purpose exactly after banging my head on this problem for a few days. It doesn't require attributes.
https://github.com/GoAheadTours/NamespaceVersioning
In summary, you can register an IApplicationModelConvention in your startup file that can iterate through controllers and register routes based on the namespaces. I created a v1 folder, and put my controller inside
The class that implements IApplicationModelConvention implements an Apply method with an ApplicationModel parameter that will have access to the Controllers in your app and their existing routes. If I see a controller does not have a route set up in my class I get the version from the namespace and use a pre-defined URL prefix to generate a route for that version.
public void Apply(ApplicationModel application) {
foreach (var controller in application.Controllers) {
var hasRouteAttribute = controller.Selectors.Any(x => x.AttributeRouteModel != null);
if (hasRouteAttribute) {
continue;
}
var nameSpace = controller.ControllerType.Namespace.Split('.');
var version = nameSpace.FirstOrDefault(x => Regex.IsMatch(x, #"[v][\d*]"));
if (string.IsNullOrEmpty(version)) {
continue;
}
controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel() {
Template = string.Format(urlTemplate, apiPrefix, version, controller.ControllerName)
};
}
}
I have all the code up on github and a link to the package on nuget as well
Use the routing attributes to control versions.
i.e.
[Route("api/v1/[controller]")]
public class BookingV1Controller : Controller
{
....
}
[Route("api/v2/[controller]")]
public class BookingV2Controller : Controller
{
....
}
For more information relating to migrating from standard Web Api and .NET Core ASP.NET have a look at: MSDN: Migrating from ASP.NET Web Api
For that Add service API versioning to your ASP.NET Core applications
public void ConfigureServices( IServiceCollection services )
{
services.AddMvc();
services.AddApiVersioning();
// remaining other stuff omitted for brevity
}
QUERYSTRING PARAMETER VERSIONING
[ApiVersion( "2.0" )]
[Route( "api/helloworld" )]
public class HelloWorld2Controller : Controller {
[HttpGet]
public string Get() => "Hello world!";
}
So this means to get 2.0 over 1.0 in another Controller with the same route, you'd go here:
/api/helloworld?api-version=2.0
we can have the same controller name with different namespaces
URL PATH SEGMENT VERSIONING
[ApiVersion( "1.0" )]
[Route( "api/v{version:apiVersion}/[controller]" )]
public class HelloWorldController : Controller {
public string Get() => "Hello world!";
}
[ApiVersion( "2.0" )]
[ApiVersion( "3.0" )]
[Route( "api/v{version:apiVersion}/helloworld" )]
public class HelloWorld2Controller : Controller {
[HttpGet]
public string Get() => "Hello world v2!";
[HttpGet, MapToApiVersion( "3.0" )]
public string GetV3() => "Hello world v3!";
}
Header Versioning
public void ConfigureServices( IServiceCollection services )
{
services.AddMvc();
services.AddApiVersioning(o => o.ApiVersionReader = new HeaderApiVersionReader("api-version"));
}
When you do HeaderApiVersioning you won't be able to just do a GET in your browser, so I'll use Postman to add the header (or I could use Curl, or WGet, or PowerShell, or a Unit Test):
Image
please refer https://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEasy.aspx