I am implementing IApiDescriptionGroupCollectionProvider to get the API description.
I have implemented in this way
private readonly IApiDescriptionGroupCollectionProvider _apiExplorer;
public RouteController(
IApiDescriptionGroupCollectionProvider apiExplorer)
{
_apiExplorer = apiExplorer;
}
[HttpGet("all")]
public IActionResult GetRoute()
{
var paths = GetApiDescriptionsFor("v1");
return Ok();
}
I want to bind all the details of the controller to an ApiRouteDocument custom model with a description of the action too. But the interface I have implemented doesn't give a summary of the action, and the controller. Is there any built-in interface to extract the summary from the actions?
I wanted to avoid the Reflection.
[ApiController]
[Route("api/contact")]
[ApiExplorerSettings(GroupName = "Contact")]
public class ContactController : ControllerBase
{
/// <summary>
/// Get by Name Contact
/// </summary>
[HttpGet("getbyname/{name}")]
public async Task<IActionResult> GetByName(string name)
{
return Ok();
}
}
public class ApiRouteDocument
{
//controllername tag
public string ControllerName { get; set; }
public string ControllerDescription { get; set; }
public IList<RoutePath> Paths;
}
public class RoutePath
{
//get
public string Method { get; set; }
//operationid
public string Name { get; set; }
//summary
public string Description { get; set; }
//path
public string Path { get; set; }
}
You have to run this code after the middleware pipeline has been established (and app.UseEndpoints() has been executed), so it should run inside a controller, or an endpoint, for example.
Because documentation comments aren't included in the assembly, you need to use attributes to annotate your classes & actions. You have the option to use the ones provided by Microsoft, such as [DisplayName], [Description], etc. under System.ComponentModel.Primitives namespace.
Or you can create your own attribute:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
internal class SummaryAttribute : Attribute
{
public string Summary { get; }
public SummaryAttribute(string summary)
{
Summary = summary;
}
}
Then inject IEnumerable<EndpointDataSource> in a controller, which will give you the list of discovered endpoints.
You can get reflected runtime type info from ControllerActionDescriptor, then extract the attributes you've added to controllers & actions.
[Description("Provides info")]
[Route("info")]
public class ThingsController : ControllerBase
{
private IEnumerable<EndpointDataSource> _endpointDataSources;
public ThingsController(IEnumerable<EndpointDataSource> endpointDataSources)
{
_endpointDataSources = endpointDataSources;
}
[Description("Returns a list of endpoints")]
[HttpGet("endpoints")]
public ActionResult Endpoints()
{
var actions = _endpointDataSources.SelectMany(it => it.Endpoints)
.OfType<RouteEndpoint>()
.Where(
it => it.Metadata
.OfType<ControllerActionDescriptor>()
.Any()
)
.Select(
e => {
var actionDescriptor = e.Metadata
.OfType<ControllerActionDescriptor>()
.First();
var isControllerIgnored = actionDescriptor.ControllerTypeInfo.GetCustomAttribute<ApiExplorerSettingsAttribute>()?.IgnoreApi ?? false;
var isActionIgnored = actionDescriptor.MethodInfo .GetCustomAttribute<ApiExplorerSettingsAttribute>()?.IgnoreApi ?? false;
return new
{
ControllerName = actionDescriptor.ControllerName,
ControllerType = actionDescriptor.ControllerTypeInfo.FullName,
ActionDescription = actionDescriptor.MethodInfo.GetCustomAttribute<DescriptionAttribute>()?.Description,
ControllerDescription = actionDescriptor.ControllerTypeInfo
.GetCustomAttribute<DescriptionAttribute>()
?.Description,
Method = actionDescriptor.EndpointMetadata.OfType<HttpMethodMetadata>()
.FirstOrDefault()
?.HttpMethods?[0],
Path = $"/{e.RoutePattern!.RawText!.TrimStart('/')}",
};
}
)
.ToList();
return Ok(actions);
}
}
when you visit /info/endpoints, you'll get a list of endpoints:
[
{
"controllerName": "Things",
"controllerType": "ApiPlayground.ThingsController",
"actionDescription": "Returns a list of endpoints",
"controllerDescription": "Provides info",
"method": "GET",
"path": "/info/endpoints"
}
]
Related
I have made a WebAPI in .NET CORE 6.
I have a controller class like this:
[ApiController]
[Route("{culture:culture}/[controller]")]
[SwaggerDefaultValue("culture", "en-US")]
[Produces("application/json")]
public class AccountsController : BaseController
{
...
}
As you can see I defined a parameter in the route like this {culture:culture}.
I would like to have a default value for this parameter in my Swagger.
I defined an attribute class like this:
[AttributeUsage(AttributeTargets.Class )]
public class SwaggerDefaultValueAttribute:Attribute
{
public string Name { get; set; }
public string Value { get; set; }
public SwaggerDefaultValueAttribute(string name, string value)
{
Name = name;
Value = value;
}
}
And a filter class like this:
public class SwaggerDefaultValueFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// needs some code
}
}
And added the filter to my Swagger service too.
options.OperationFilter<SwaggerDefaultValueFilter>();
However, the problem is most of the code samples that I found are related to the old versions of Swagger and most of their methods are deprecated (like this one).
The question is, how can I modify this SwaggerDefaultValueFilter class to show a default value in my path parameter:
FYI: I am using <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.2.3" />.
I found this sample too, however, it does not set the default values of the path parameters, it seems it works for model attributes.
To assign the default value for the parameter, you can do something like this,
internal static class OperationFilterContextExtensions
{
public static IEnumerable<T> GetControllerAndActionAttributes<T>(this OperationFilterContext context) where T : Attribute
{
var controllerAttributes = context.MethodInfo.DeclaringType.GetTypeInfo().GetCustomAttributes<T>();
var actionAttributes = context.MethodInfo.GetCustomAttributes<T>();
var result = new List<T>(controllerAttributes);
result.AddRange(actionAttributes);
return result;
}
}
public class SwaggerDefaultValueFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var defaultAttributes = context.GetControllerAndActionAttributes<SwaggerDefaultValueAttribute>();
if (defaultAttributes.Any())
{
foreach (var defaultAttribute in defaultAttributes)
{
var parameter = operation.Parameters.FirstOrDefault(d => d.Name == defaultAttribute.Name);
if (parameter != null)
{
version.Schema.Default = new OpenApiString(defaultAttribute.Value);
}
}
}
}
}
Not Swashbuckle.AspNetCore but this might send you on the right path:
Controller:
https://github.com/heldersepu/Swagger-Net-Test/blob/master/Swagger_Test/Controllers/CompanyController.cs#L35-L39
[Route("Get2")]
public Company Get2([FromUri] Company c)
{
return c;
}
Model:
https://github.com/heldersepu/Swagger-Net-Test/blob/master/Swagger_Test/Models/Company.cs
public class Company
{
/// <summary>The Unique Company ID</summary>
/// <example>123</example>
[Required]
[DefaultValue(456)]
public int Id { get; set; }
[Required]
[DefaultValue(null)]
public int? MyId { get; set; }
/// <summary>The Company Name</summary>
/// <example>Acme co</example>
[DefaultValue("My Company")]
public string Name { get; set; }
Live code:
http://swagger-net-test.azurewebsites.net/swagger/ui/index?filter=Company#/Company/Company_Get2
We have a Web API written in DotNet Core 3.1.402 (I am new to DotNet Core and WebAPI).
We use SqlKata for Database processing.
We have an Account model that has AccountID, AccountName, AccountNumber, etc.
We would like to get an Account by different attributes, for ex: by AccountID, by AccountName, by AccountNumber.
How can we do that so that we don't need a separate HttpGet for each attribute (so we don't have to repeat the same code for different attributes) ?
This is our HttpGet in the AccountsController to get the account by AccountID
public class AccountsController : ControllerBase
{
private readonly IAccountRepository _accountRepository;
[HttpGet("{AccountID}")]
public Account GetAccount(int AccountID)
{
var result = _accountRepository.GetAccount(AccountID);
return result;
}
This is the code in the AccountRepository.cs
public Account GetAccount(int accountID)
{
var result = _db.Query("MyAccountTable").Where("AccountID", accountID).FirstOrDefault<Account>();
return result;
}
This is the Account class
namespace MyApi.Models
{
public class Account
{
public string AccountID { get; set; }
public string AccountName { get; set; }
public string AccountNumber { get; set; }
// other attributes
}
}
Thank you.
Doing it with GET can be a pain, there are ways to pass on the path/query arrays and complex objects but are ugly, the best you can do is to use POST instead of GET and pass an object with the filters that you want.
//In the controller...
[HttpPost]
public Account GetAccount([FromBody]Filter[] DesiredFilters)
{
var result = _accountRepository.GetAccount(DesiredFilters);
return result;
}
//Somewhere else, in a shared model...
public class Filter
{
public string PropertyName { get; set; }
public string Value { get; set; }
}
//In the repository...
public Account GetAccount(Filter[] Filters)
{
var query = _db.Query("MyAccountTable");
foreach(var filter in Filters)
query = query.Where(filter.PropertyName, filter.Value);
return query.FirstOrDefault<Account>();
}
Now you can send a JSON array on the request body with any filters that you want, per example:
[
{ "PropertyName": "AccountID", "Value": "3" },
{ "PropertyName": "AccountName", "Value": "Whatever" }
]
I'm curious if it's possible to bind a query string that is passed in with a GET request to a Model.
For example, if the GET url was https://localhost:1234/Users/Get?age=30&status=created
Would it be possible on the GET action to bind the query parameters to a Model like the following:
[HttpGet]
public async Task<JsonResult> Get(UserFilter filter)
{
var age = filter.age;
var status = filter.status;
}
public class UserFilter
{
public int age { get; set; }
public string status { get; set; }
}
I am currently using ASP.NET MVC and I have done quite a bit of searching but the only things I can find are related to ASP.NET Web API. They suggest using the [FromUri] attribute but that is not available in MVC.
I just tested the this, and it does work (at least in .net core 3.1)
[HttpGet("test")]
public IActionResult TestException([FromQuery]Test test)
{
return Ok();
}
public class Test
{
public int Id { get; set; }
public string Yes { get; set; }
}
You can can create an ActionFilterAttribute where you will parse the query parameters, and bind them to a model. And then you can decorate your controller method with that attribute.
For example
public class UserFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var controller = actionContext.ControllerContext.Controller as CustomApiController;
var queryParams = actionContext.Request.GetQueryNameValuePairs();
var ageParam = queryParams.SingleOrDefault(a => a.Key == "age");
var statusParam = queryParams.SingleOrDefault(a => a.Key == "status");
controller.UserFilter = new UserFilter {
Age = int.Parse(ageParam.Value),
Status = statusParam.Value
};
}
}
The CustomApiController (inherits from your current controller) and has a UserFilter property so you can keep the value there. You can also add null checks in case some of the query parameters are not sent with the request..
Finally you can decorate your controller method
[HttpGet]
[UserFilter]
public async Task<JsonResult> Get()
{
var age = UserFilter.age;
var status = UserFilter.status;
}
public class UserController : ApiController
{
UserSampleEntities entities = new UserSampleEntities();
// GET api/<controller>
[Route("api/User")]
public IEnumerable<user> Get()
{
{
return entities.users;
}
}
}
This returns the json with all the entries in the database with all its properties. How do I filter such that I can obtain a json for only specific properties?
Create a new class that represents a user using only the properties you want to expose from "api/User":
public class UserDto
{
public int Foo { get; set; }
public string Bar { get; set; }
// add the properties you need here
}
Rewrite your API action to this:
[Route("api/User")]
public IEnumerable<UserDto> Get()
{
return entities.users
.Select(u => new UserDto
{
Foo = u.Foo,
Bar = u.Bar,
// map the properties you need here
})
.ToArray();
}
I am building WebApi2 project to expose some RESTful service. Let's say I have following model objects.
public class Person
{
public string Name { get; set; }
public DateTime? Birthdate { get; set; }
public int Status { get; set; }
public List<Account> Accounts { get; set; }
}
public class Account
{
public decimal Amount { get; set; }
public string Code { get; set; }
public DateTime Expiry { get; set; }
}
In my service I have to go to 2 different systems to retrieve data for Person and the account info of the Person. Obviously the service implementation looks like
[HttpGet]
[Route("Person/{id:int}")]
public IHttpActionResult Get(string id)
{
var person = new Person();
person = GetPersonFromSystemA(id);
if (person.Status == 2)
{
person.Accounts = GetPersonAccountsFromSystemB(id);
}
return this.Ok(person);
}
I cannot use EF at all in this case, so OData is very tricky.
I have some requirement that I need to provide the filtering capability to the service client. The client can decide which fields of the objects to return, it also means that if the client does not like to include Accounts info of the person I should skip the second call to system B to avoid entire child object.
I did some quick search but I could not find some similar solution yet. I know I can implement my own filtering syntax, and have all custom codes to use the filtering (by having lots of if/else).
I am looking for some ideas of more elegant solution.
Entity Framework is not required for building an OData Service. If you do not use OData, you will probably have to implement your own IQueryable which is what OData does out of the box.
Some sample code.
Model classes with some added properties
public class Person
{
[Key]
public String Id { get; set; }
[Required]
public string Name { get; set; }
public DateTime? Birthdate { get; set; }
public int Status { get; set; }
public List<Account> Accounts { get; set; }
}
public class Account
{
[Key]
public String Id { get; set; }
[Required]
public decimal Amount { get; set; }
public string Code { get; set; }
public DateTime Expiry { get; set; }
}
WebApiConfig.cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapODataServiceRoute("odata", null, GetEdmModel(), new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));
config.EnsureInitialized();
}
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.Namespace = "YourNamespace";
builder.ContainerName = "DefaultContainer";
builder.EntitySet<Person>("People");
builder.EntitySet<Account>("Accounts");
var edmModel = builder.GetEdmModel();
return edmModel;
}
}
Controller method
[EnableQuery]
public class PeopleController : ODataController
{
public IHttpActionResult Get()
{
return Ok(SomeDataSource.Instance.People.AsQueryable());
}
}
You will need to include the Microsoft.AspNet.OData Nuget package.
Refer to the following for more guidance. It uses an in memory data source, but the concept is the same regardless.
http://www.odata.org/blog/how-to-use-web-api-odata-to-build-an-odata-v4-service-without-entity-framework/
When building a web api you would often want to filter your response and get only certain fields. You could do it in many ways, one of which as suggested above. Another way, you could approach it is using data shaping in your web api.
If you had a controller action as such:
public IHttpActionResult Get(string fields="all")
{
try
{
var results = _tripRepository.Get();
if (results == null)
return NotFound();
// Getting the fields is an expensive operation, so the default is all,
// in which case we will just return the results
if (!string.Equals(fields, "all", StringComparison.OrdinalIgnoreCase))
{
var shapedResults = results.Select(x => GetShapedObject(x, fields));
return Ok(shapedResults);
}
return Ok(results);
}
catch (Exception)
{
return InternalServerError();
}
}
And then your GetShapedData method can do the filtering as such:
public object GetShapedObject<TParameter>(TParameter entity, string fields)
{
if (string.IsNullOrEmpty(fields))
return entity;
Regex regex = new Regex(#"[^,()]+(\([^()]*\))?");
var requestedFields = regex.Matches(fields).Cast<Match>().Select(m => m.Value).Distinct();
ExpandoObject expando = new ExpandoObject();
foreach (var field in requestedFields)
{
if (field.Contains("("))
{
var navField = field.Substring(0, field.IndexOf('('));
IList navFieldValue = entity.GetType()
?.GetProperty(navField, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public)
?.GetValue(entity, null) as IList;
var regexMatch = Regex.Matches(field, #"\((.+?)\)");
if (regexMatch?.Count > 0)
{
var propertiesString = regexMatch[0].Value?.Replace("(", string.Empty).Replace(")", string.Empty);
if (!string.IsNullOrEmpty(propertiesString))
{
string[] navigationObjectProperties = propertiesString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
List<object> list = new List<object>();
foreach (var item in navFieldValue)
{
list.Add(GetShapedObject(item, navigationObjectProperties));
}
((IDictionary<string, object>)expando).Add(navField, list);
}
}
}
else
{
var value = entity.GetType()
?.GetProperty(field, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public)
?.GetValue(entity, null);
((IDictionary<string, object>)expando).Add(field, value);
}
}
return expando;
}
Check my blog for a detailed post: https://jinishbhardwaj.wordpress.com/2016/12/03/web-api-supporting-data-shaping/