WebAPI Url.Link() returning NULL - asp.net-web-api2

I am having some trouble in generating WebAPI hypermedia links in a project I am working on.
In the code snippet below the extension methods Order, OrderUpdate and OrderDelete for individual orders are all returning Null links. I suspect this is because WebAPI is unable to resolve these inner routes from the parent Orders route.
I am not certain why Null links are being returned as I am supplying valid route names to the Link method.
You can see from the resulting JSON output that the Uri elements are all Null for the Links array element within the Order object.
// WebApiConfig.cs
config.Routes.MapHttpRoute(
name: "Orders",
routeTemplate: "api/orders",
defaults: new { controller = "order", action = "get" },
constraints: new { httpMethod = new HttpMethodConstraint( HttpMethod.Get ) }
);
config.Routes.MapHttpRoute(
name: "Order",
routeTemplate: "api/orders/{orderid}",
defaults: new { controller = "order" },
constraints: new { orderid = "^[0-9]+$", httpMethod = new HttpMethodConstraint( HttpMethod.Get ) }
);
config.Routes.MapHttpRoute(
name: "OrderUpdate",
routeTemplate: "api/orders/{orderid}",
defaults: new { controller = "order", action = "put" },
constraints: new { orderid = "^[0-9]+$", httpMethod = new HttpMethodConstraint( HttpMethod.Put ) }
);
// OrderController.cs
public async Task<Orders> Get()
{
var orders = new Orders();
orders.Links.OrderList( Url, "self" ); // returns a link OK
orders.Links.OrderAdd( Url, "order-create" ); // returns a link OK
foreach ( var order in await _repository.GetOrders() )
{
order.Links.Order( Url, "self", order.Id ); // returns NULL
if ( !User.IsInRole( "Readonly" ) )
{
order.Links.OrderUpdate( Url, "order-update", order.Id ); // returns NULL
order.Links.OrderDelete( Url, "order-delete", order.Id ); // returns NULL
}
orders.Order.Add( order );
}
return orders;
}
// LinkGenerationExtensions.cs
public static void Order( this List<Link> #this, UrlHelper url, string rel, int orderId )
{
#this.Add( new Link { Rel = rel, Uri = url.OrderUri( orderId ) } );
}
public static string OrderUri( this UrlHelper url, int orderId )
{
return url.Link( "Order", new { id = orderId } );
}
public static void OrderList( this List<Link> #this, UrlHelper url, string rel )
{
#this.Add( new Link { Rel = rel, Uri = url.OrderListUri() } );
}
public static string OrderListUri( this UrlHelper url )
{
return url.Link( "Orders", new { } );
}
public static void OrderUpdate( this List<Link> #this, UrlHelper url, string rel, int orderId )
{
#this.Add( new Link { Rel = rel, Uri = url.OrderUpdateUri( orderId ) } );
}
public static string OrderUpdateUri( this UrlHelper url, int orderId )
{
return url.Link( "OrderUpdate", new { id = orderId } );
}
The above code generates the following JSON response:
{
"Order": [
{
"Id": 4,
"OrderRef": 123456,
"OrderDate": "2015-02-04T10:28:00",
"CustomerName": "ACME Construction Ltd",
"OrderedBy": "PA",
"InstallationDate": "2015-06-15T00:00:00",
"Address": "900 ACME Street",
"Postcode": "SW2 7AX",
"Town": "London",
"OrderNumber": "6508756191",
"Value": 525,
"Invoice": 0,
"Links": [
{
"Rel": "self",
"Uri": null
},
{
"Rel": "order-update",
"Uri": null
},
{
"Rel": "order-delete",
"Uri": null
}
]
}
],
"Links": [
{
"Rel": "self",
"Uri": "http://localhost/Site/api/orders"
},
{
"Rel": "order-create",
"Uri": "http://localhost/Site/api/orders/add"
}
]
}

I've answered my own question after discovering that the parameter names in the route values must match those of their defined routes.
Replacing id with orderid did the job.
Here is the updated code:
// LinkGenerationExtensions.cs
public static void Order( this List<Link> #this, UrlHelper url, string rel, int orderId )
{
#this.Add( new Link { Rel = rel, Uri = url.OrderUri( orderId ) } );
}
public static string OrderUri( this UrlHelper url, int orderId )
{
return url.Link( "Order", new { orderid = orderId } );
}
public static void OrderList( this List<Link> #this, UrlHelper url, string rel )
{
#this.Add( new Link { Rel = rel, Uri = url.OrderListUri() } );
}
public static string OrderListUri( this UrlHelper url )
{
return url.Link( "Orders", new { } );
}
public static void OrderUpdate( this List<Link> #this, UrlHelper url, string rel, int orderId )
{
#this.Add( new Link { Rel = rel, Uri = url.OrderUpdateUri( orderId ) } );
}
public static string OrderUpdateUri( this UrlHelper url, int orderId )
{
return url.Link( "OrderUpdate", new { orderid = orderId } );
}
public static void OrderDelete( this List<Link> #this, UrlHelper url, string rel, int orderId )
{
#this.Add( new Link { Rel = rel, Uri = url.OrderDeleteUri( orderId ) } );
}
public static string OrderDeleteUri( this UrlHelper url, int orderId )
{
return url.Link( "OrderDelete", new { orderid = orderId } );
}

Had this problem on VS2019. Solved when I upgraded the project to .NET5 and all Microsoft nuget packages to v5.x.

Related

.net core 2.2 webapi and handling multypart request

I have .net core 2.2 webapi as backend.
My backend have to handling the requests (photo and text) from mobile application as multypart-form request.
Plz hint me how it to prpvide on backend?
this problem has solved
public class SwaggerFileOperationFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
if (operation.OperationId == "MailWithPhotos")
{
operation.Parameters = new List<IParameter>
{
new NonBodyParameter
{
Name = "awr_file",
Required = false,
Type = "file",
In = "formData"
},
new NonBodyParameter
{
Name = "awr_message",
Required = true,
Type = "string",
In = "formData"
},
new NonBodyParameter
{
Type = "string",
In = "header",
Name = "Authorization",
Description = "token",
Required = true
}
};
}
}
}
[HttpPost]
[ProducesResponseType(typeof(ResponseMail), 200)]
public async Task<PipeResponse> MailWithPhotos([FromForm] MailwithPhoto fIleUploadAPI)
{
var file = fIleUploadAPI.awr_file; // OK!!!
var message = fIleUploadAPI.awr_message; // OK!!!
var tokenA = Request.Headers["Authorization"]; // OK!!!
var fileContentStream11 = new MemoryStream();
await fIleUploadAPI.awr_file.CopyToAsync(fileContentStream11);
await System.IO.File.WriteAllBytesAsync(Path.Combine(folderPath,
fIleUploadAPI.awr_file.FileName),
fileContentStream11.ToArray());
}
public class MailwithPhoto
{
public string awr_message { get; set; }
public string Authorization { get; set; }
public IFormFile awr_file { get; set; }
}

Swagger versioning is not working. It displays all endpoints, despite the selected API version

all!
I am using Swagger in ASP.NET Core 3.1 application.
I need to create an endpoint for the new version of API and with the same route as a previous version.
My controller is:
namespace Application.Controllers
{
[ApiVersion("1")]
[ApiVersion("2")]
[ApiController]
[Route("api/v{version:apiVersion}")]
public class CustomController: ControllerBase
{
[HttpGet]
[Route("result")]
public IActionResult GetResult()
{
return Ok("v1")
}
[HttpGet]
[MapToApiVersion("2")]
[Route("result")]
public IActionResult GetResult(int number)
{
return Ok("v2")
}
}
}
My configuration:
services.AddApiVersioning(
options =>
{
options.ReportApiVersions = true;
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc($"v1", new OpenApiInfo { Title = "api1", Version = $"v1" });
c.SwaggerDoc($"v2", new OpenApiInfo { Title = "api2", Version = $"v2" });
c.OperationFilter<RemoveVersionParameterFilter>();
c.DocumentFilter<ReplaceVersionWithExactValueInPathFilter>();
c.EnableAnnotations();
});
app.UseSwagger().UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"/swagger/v1/swagger.json", $"api1 v1");
c.SwaggerEndpoint($"/swagger/v2/swagger.json", $"api2 v2");
});
After loading I get an error: Fetch error undefined /swagger/v1/swagger.json
But If I change the second route to the "resutlTwo", I can observe both endpoints in swagger, ignoring current version (api1 v1 or api2 v2)
How can I see only 1 endpoint per API version?
Thanks Roar S. for help!
I just added
services.AddApiVersioning(apiVersioningOptions =>
{
apiVersioningOptions.ReportApiVersions = true;
apiVersioningOptions.ApiVersionReader = new UrlSegmentApiVersionReader();
});
and
c.DocInclusionPredicate((version, desc) =>
{
var endpointMetadata = desc.ActionDescriptor.EndpointMetadata;
if (!desc.TryGetMethodInfo(out MethodInfo methodInfo))
{
return false;
}
var specificVersion = endpointMetadata
.Where(data => data is MapToApiVersionAttribute)
.SelectMany(data => (data as MapToApiVersionAttribute).Versions)
.Select(apiVersion => apiVersion.ToString())
.SingleOrDefault();
if (!string.IsNullOrEmpty(specificVersion))
{
return $"v{specificVersion}" == version;
}
var versions = endpointMetadata
.Where(data => data is ApiVersionAttribute)
.SelectMany(data => (data as ApiVersionAttribute).Versions)
.Select(apiVersion => apiVersion.ToString());
return versions.Any(v => $"v{v}" == version);
});
And it split endpoints to different files.
I just tested your case with this setup.You are missing UrlSegmentApiVersionReader.
public class SwaggerOptions
{
public string Title { get; set; }
public string JsonRoute { get; set; }
public string Description { get; set; }
public List<Version> Versions { get; set; }
public class Version
{
public string Name { get; set; }
public string UiEndpoint { get; set; }
}
}
In Startup#ConfigureServices
// Configure versions
services.AddApiVersioning(apiVersioningOptions =>
{
apiVersioningOptions.ReportApiVersions = true;
apiVersioningOptions.ApiVersionReader = new UrlSegmentApiVersionReader();
});
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(swaggerGenOptions =>
{
var swaggerOptions = new SwaggerOptions();
Configuration.GetSection("Swagger").Bind(swaggerOptions);
foreach (var currentVersion in swaggerOptions.Versions)
{
swaggerGenOptions.SwaggerDoc(currentVersion.Name, new OpenApiInfo
{
Title = swaggerOptions.Title,
Version = currentVersion.Name,
Description = swaggerOptions.Description
});
}
swaggerGenOptions.DocInclusionPredicate((version, desc) =>
{
if (!desc.TryGetMethodInfo(out MethodInfo methodInfo))
{
return false;
}
var versions = methodInfo.DeclaringType.GetConstructors()
.SelectMany(constructorInfo => constructorInfo.DeclaringType.CustomAttributes
.Where(attributeData => attributeData.AttributeType == typeof(ApiVersionAttribute))
.SelectMany(attributeData => attributeData.ConstructorArguments
.Select(attributeTypedArgument => attributeTypedArgument.Value)));
return versions.Any(v => $"{v}" == version);
});
swaggerGenOptions.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"));
... some filter settings here
});
In Startup#Configure
var swaggerOptions = new SwaggerOptions();
Configuration.GetSection("Swagger").Bind(swaggerOptions);
app.UseSwagger(option => option.RouteTemplate = swaggerOptions.JsonRoute);
app.UseSwaggerUI(option =>
{
foreach (var currentVersion in swaggerOptions.Versions)
{
option.SwaggerEndpoint(currentVersion.UiEndpoint, $"{swaggerOptions.Title} {currentVersion.Name}");
}
});
appsettings.json
{
"Swagger": {
"Title": "App title",
"JsonRoute": "swagger/{documentName}/swagger.json",
"Description": "Some text",
"Versions": [
{
"Name": "2.0",
"UiEndpoint": "/swagger/2.0/swagger.json"
},
{
"Name": "1.0",
"UiEndpoint": "/swagger/1.0/swagger.json"
}
]
}
}
This code is very similar to a related issue I'm working on here on SO.

Swashbuckle.AspNetCore 5.0.0-rc4 UploadFileFilter doesn't work

I need to add upload file for Swashbuckle.AspNetCore 5.0.0-rc4. In earlier version it works like:
public class SwaggerUploadFileParametersFilter : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
if (operation.parameters != null)
{
var attribute =
apiDescription.ActionDescriptor.GetCustomAttributes<UploadFileParametersAttribute>()
.FirstOrDefault();
if (attribute != null)
{
operation.consumes.Add("multipart/form-data");
operation.parameters.Add(new Parameter
{
name = "file",
required = true,
type = "file",
#in = "formData"
}
);
}
}
}
}
[UploadFileParameters]
public async Task<IHttpActionResult> MyMethod([FromUri]MyMethodParams parameters)
I try to implement it using Microsoft.OpenApi.Models objects:
public class SwaggerUploadFileParametersFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var actionAttributes = context.MethodInfo.GetCustomAttributes<UploadFileParametersAttribute>().FirstOrDefault();
if (actionAttributes != null)
{
operation.RequestBody = new OpenApiRequestBody()
{
Content =
{
["multipart/form-data"] = new OpenApiMediaType()
{
Schema = new OpenApiSchema()
{
Properties =
{
["file"] = new OpenApiSchema()
{
Description = "Select file",
Type = "file"
}
}
}
}
}
};
}
}
}
But it doesn't work. I don't see file component in swagger
I took your code and some documentation from Swagger File Upload
I modified your code and added small fix
public class SwaggerUploadFileParametersFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var parameters = operation.Parameters;
if (parameters == null || parameters.Count == 0)
{
return;
}
var isUploadFile = context.ApiDescription.ActionDescriptor.Parameters.Any(x => x.ParameterType == typeof(IFormFile));
if (isUploadFile)
{
operation.RequestBody = new OpenApiRequestBody()
{
Content =
{
["multipart/form-data"] = new OpenApiMediaType()
{
Schema = new OpenApiSchema()
{
Type = "object",
Properties =
{
["file"] = new OpenApiSchema()
{
Description = "Select file", Type = "string", Format = "binary"
}
}
}
}
}
};
}
}
}
And controller:
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<IActionResult> UploadFileAsync([FromForm] IFormFile file)

Elastisearch.net property opt in?

I'm using Elastisearch.NET with NEST 2.3. I want to use attribute mapping but I only want to index certain properties. As I understand it all properties are indexed unless you ignore them using for example [String(Ignore = true)] Is it possible to ignore all properties by default and only index the ones that have a nest attribute attached to them? Like JSON.NETs MemberSerialization.OptIn
You could do this using a custom serializer to ignore any properties not marked with a NEST ElasticsearchPropertyAttributeBase derived attribute.
void Main()
{
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var connectionSettings = new ConnectionSettings(
pool,
new HttpConnection(),
new SerializerFactory(s => new CustomSerializer(s)));
var client = new ElasticClient(connectionSettings);
client.CreateIndex("demo", c => c
.Mappings(m => m
.Map<Document>(mm => mm
.AutoMap()
)
)
);
}
public class Document
{
[String]
public string Field1 { get; set;}
public string Field2 { get; set; }
[Number(NumberType.Integer)]
public int Field3 { get; set; }
public int Field4 { get; set; }
}
public class CustomSerializer : JsonNetSerializer
{
public CustomSerializer(IConnectionSettingsValues settings, Action<JsonSerializerSettings, IConnectionSettingsValues> settingsModifier) : base(settings, settingsModifier) { }
public CustomSerializer(IConnectionSettingsValues settings) : base(settings) { }
public override IPropertyMapping CreatePropertyMapping(MemberInfo memberInfo)
{
// if cached before, return it
IPropertyMapping mapping;
if (Properties.TryGetValue(memberInfo.GetHashCode(), out mapping))
return mapping;
// let the base method handle any types from NEST
// or Elasticsearch.Net
if (memberInfo.DeclaringType.FullName.StartsWith("Nest.") ||
memberInfo.DeclaringType.FullName.StartsWith("Elasticsearch.Net."))
return base.CreatePropertyMapping(memberInfo);
// Determine if the member has an attribute
var attributes = memberInfo.GetCustomAttributes(true);
if (attributes == null || !attributes.Any(a => typeof(ElasticsearchPropertyAttributeBase).IsAssignableFrom(a.GetType())))
{
// set an ignore mapping
mapping = new PropertyMapping { Ignore = true };
Properties.TryAdd(memberInfo.GetHashCode(), mapping);
return mapping;
}
// Let base method handle remaining
return base.CreatePropertyMapping(memberInfo);
}
}
which produces the following request
PUT http://localhost:9200/demo?pretty=true
{
"mappings": {
"document": {
"properties": {
"field1": {
"type": "string"
},
"field3": {
"type": "integer"
}
}
}
}
}

Serialization empty

I try to serialize a custom object, but it ends up being empty.
The result I want to have is :
{
"accessRules": [
{
"method": "GET",
"path": "/*"
}
],
"redirection":"https://www.mywebsite.com/"
}'
Here are my classes:
[Serializable]
public class AccessRule
{
[SerializeAs(Name = "method")]
string Method { get; set; }
[SerializeAs(Name = "path")]
string Path { get; set; }
public AccessRule(string method, string path)
{
this.Method = method;
this.Path = path;
}
}
[Serializable]
public class RequestBody
{
[SerializeAs(Name = "accessRules")]
AccessRule[] AccessRules
{
get { return arList.ToArray(); }
}
private List<AccessRule> arList;
[SerializeAs(Name = "redirection")]
string Redirection { get; set; }
public RequestBody(string method, string path, string redirection)
{
this.arList = new List<AccessRule>();
this.arList.Add(new AccessRule(method, path));
this.Redirection = redirection;
}
}
var RequestBody = new RequestBody("GET", "/*", "https://www.mywebsite.com");
And it leads to an empty json string.
If i do :
var RequestBody = new { accessRules = new[] { new { method = "GET", path = "/*" } }, redirection = "https://www.mywebsite.com" };
It works.
I use RestSharp 104.4.0.0, also tried with Newtonsoft.Json v6.0.0.0
What am I missing here ?
Thanks.