How do I Get with using string parameter? - asp.net-web-api

I currently have a web api Get method using EF6 and it is accepting an int parameter called serial. Instead of the primary key serial I want to find extserial from the URI.
Here is my Get where I have localhost/api/AH?serial=1
// GET api/AH/5
[ResponseType(typeof(Transmital))]
public IHttpActionResult GetTransmital(int Serial)
{
Transmital transmital = db.Transmitals.Find(Serial);
if (transmital == null)
{
return NotFound();
}
return Ok(transmital);
}
I need to have it get by localhost/api/AH?ExtSerial=ABC123
For some reason it won't take the following
// GET api/AH/5
[ResponseType(typeof(Transmital))]
public IHttpActionResult GetTransmital(string ExtSerial) //or [FromUri]string ExtSerial
{
Transmital transmital = db.Transmitals.Find(ExtSerial);
if (transmital == null)
{
return NotFound();
}
return Ok(transmital);
}

You should use IHttpActionResult interface.
Try this:
public IHttpActionResult GetTransmitalsExtSerial([FromUri] string ExtSerial)
{
//return Ok(ExtSerial.ToString());
return Ok(db.Transmitals.Where(exs => exs.ExtSerial == ExtSerial));
}

Related

How to define dependencies on two client calls in quarkus reactive programming

I have two Client APIs that return an Uni.
Uni<Customer> getCustomer(customerID)
Uni<Address> getAddress(addressID)
And I want to open a REST API
Uni<FullCustomer> getFullCustomer(String customerID)
The logic is to make the Customer Client call first. If the returned customer object has addressID then make the second Address Client call and get shipping address details. If shipping address is not available then just wrap the customer in FullCustomer object and return else wrap both customer and address in FullCustomer object and return.
I dont want to block the thread on client call (await().indefinitely()), hence i am using onItem and transfer method call. But my code returns a Uni<Uni> and i want it to return a Uni.
#GET
#Path("api/customer/{id}")
#Produces({ "application/json" })
Uni<Uni<FullCustomer>> getFullCustomer(#PathParam("id") String customerID){
Uni<Customer> customerResponse = getCustomer(customerID);
Uni<Uni<FullCustomer>> asyncResponse = customerResponse.onItem().transform(customer -> {
if (customer.getAddressId() != null) {
Uni<Address> addressResponse = getAddress(customer.getAddressId());
Uni<FullCustomer> fullCustomer = addressResponse.onItem().transform(address -> {
if (address.getShippingAddress() != null) {
return new FullCustomer(customer, address.getShippingAddress());
} else {
return new FullCustomer(customer);
}
});
}
return Uni.createFrom().item(new FullCustomer(customer));
});
return asyncResponse;
}
How can I rewrite my code so that it returns Uni keeping reactive ( async client ) calls
Got the solution. Thanks Ladicek for comments.
public Uni<FullCustomer> getFullCustomer(#PathParam("id") String customerID) {
return getCustomer(customerID)
.onItem()
.transformToUni(customer -> {
if (customer.getAddressId() != null) {
return getAddress(customer.getAddressId()).onItem().transform(address -> {
if (address.getShippingAddress() != null) {
return new FullCustomer(customer, address.getShippingAddress());
} else {
return new FullCustomer(customer);
}
});
} else {
return Uni.createFrom().item(new FullCustomer(customer));
}
});
}

ASP.NET WebApi2 OData handling of queries with slash /

I have made a "standard" Web Api 2 OData project with convention model routing. Following OData queries are working:
/odata/Users
/odata/Users(123)
/odata/$metadata
/odata/Users?$select=Username
So everything seemed to be fine until I tried this, which I think is also a legal OData query:
/odata/Users(123)/Username
Slash / in query breaks everything and it does not hit the controller class and OData authentication flow at all. Should this be supported at all in Microsoft ASP.NET OData implementation? Or is this supported only if I define explicit methods with correct routes for every single property like Username? Any suggestions to fix this? I have tried explicit {*rest} routes etc.
AFAIK, the built-in routing conventions don't include one for property access. You'd be required to add many actions for every property access.
However, based on this resource here, it's not all that difficult to add a custom routing convention to handle the property access path template: ~/entityset/key/property
Here's a custom routing convention adapted from the link I shared above
Assembly used: Microsoft.AspNet.OData 7.4.1 - the approach would be the same for any other OData Web API library you might be using
Class used for illustration
public class Product
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
Add routing convention for property access
// Usings
using Microsoft.AspNet.OData.Routing;
using Microsoft.AspNet.OData.Routing.Conventions;
using System;
using System.Linq;
using System.Web.Http.Controllers;
// ...
public class CustomPropertyRoutingConvention : NavigationSourceRoutingConvention
{
private const string ActionName = "GetProperty";
public override string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap)
{
if (odataPath == null || controllerContext == null || actionMap == null)
{
return null;
}
if (odataPath.PathTemplate == "~/entityset/key/property" ||
odataPath.PathTemplate == "~/entityset/key/cast/property" ||
odataPath.PathTemplate == "~/singleton/property" ||
odataPath.PathTemplate == "~/singleton/cast/property")
{
var segment = odataPath.Segments.OfType<Microsoft.OData.UriParser.PropertySegment>().LastOrDefault();
if (segment != null)
{
string actionName = FindMatchingAction(actionMap, ActionName);
if (actionName != null)
{
if (odataPath.PathTemplate.StartsWith("~/entityset/key", StringComparison.Ordinal))
{
var keySegment = odataPath.Segments.OfType<Microsoft.OData.UriParser.KeySegment>().FirstOrDefault();
if (keySegment == null || !keySegment.Keys.Any())
throw new InvalidOperationException("This link does not contain a key.");
controllerContext.RouteData.Values[ODataRouteConstants.Key] = keySegment.Keys.First().Value;
}
controllerContext.RouteData.Values["propertyName"] = segment.Property.Name;
return actionName;
}
}
}
return null;
}
public static string FindMatchingAction(ILookup<string, HttpActionDescriptor> actionMap, params string[] targetActionNames)
{
foreach (string targetActionName in targetActionNames)
{
if (actionMap.Contains(targetActionName))
{
return targetActionName;
}
}
return null;
}
}
Add single method in your controller to handle request for any property
public class ProductsController : ODataController
{
// ...
[HttpGet]
public IHttpActionResult GetProperty(int key, string propertyName)
{
var product = _db.Products.FirstOrDefault(d => d.Id.Equals(key));
if (product == null)
{
return NotFound();
}
PropertyInfo info = typeof(Product).GetProperty(propertyName);
object value = info.GetValue(product);
return Ok(value, value.GetType());
}
private IHttpActionResult Ok(object content, Type type)
{
var resultType = typeof(OkNegotiatedContentResult<>).MakeGenericType(type);
return Activator.CreateInstance(resultType, content, this) as IHttpActionResult;
}
// ...
}
In your WebApiConfig.cs (or equivalent place where you configure the service)
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Product>("Products");
var routingConventions = ODataRoutingConventions.CreateDefaultWithAttributeRouting("odata", configuration);
routingConventions.Insert(0, new CustomPropertyRoutingConvention());
configuration.MapODataServiceRoute("odata", "odata", modelBuilder.GetEdmModel(), new DefaultODataPathHandler(), routingConventions);
configuration.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
configuration.EnsureInitialized();
Request for Name property: /Products(1)/Name
Request for Id property: /Products(1)/Id

Forbid users from executing WebApi actions

I have the following WebApi action that deletes an order from the back-end database, only for users that are in the Admin and Order roles. However, if the user is also in the Readonly role the action returns a HTTP 403 Forbidden response.
[Authorize(Roles = "Admin,Order")]
public async Task<IHttpActionResult> Delete(int orderid) {
if(User.IsInRole("Readonly")) { return Forbidden(); }
var order = await _repository.Get(orderid);
if(order != null) {
await _repository.Delete(orderid);
return NoContent();
}
else {
return NotFound();
}
}
What I'd like to know is it possible to prevent actions from being executed if users are in specific roles so that I do not have to put if(User.IsInRole("Readonly")) { return Forbidden(); } at the start of all database update-able action methods, e.g.
[Authorize(Roles = "Admin,Order")]
[NotAuthorized(Roles = "Readonly")]
public async Task<IHttpActionResult> Delete(int orderid) {
var order = await _repository.Get(orderid);
if(order != null) {
await _repository.Delete(orderid);
return NoContent();
}
else {
return NotFound();
}
}
The NotAuthorized action filter will return a HTTP 403 Forbidden response if the user is in the Readonly role.
Is this possible?
This is the code to implement a reverse of the [Authorize()] attribute and forbid users from executing MVC WebApi actions if they are a member of one or more roles.
using System;
using System.Net;
using System.Net.Http;
using System.Security.Principal;
using System.Web.Http;
using System.Web.Http.Controllers;
namespace MyAPI {
[AttributeUsage(AttributeTargets.Method,AllowMultiple = false)]
public class NotAuthorizedAttribute : AuthorizeAttribute {
public override void OnAuthorization(HttpActionContext actionContext) {
IPrincipal user = actionContext.RequestContext.Principal;
if(!user.Identity.IsAuthenticated) {
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
else {
bool userInRole = false;
foreach(var role in Roles.Split(',')) {
if(user.IsInRole(role)) {
userInRole = true;
break;
}
}
if(userInRole) {
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
}
}
}
}
}
To use this filter attribute simply decorate any actions that you don't want users to execute if they're a member of a restricted role, e.g. if the user is part of a read-only role they not permitted to update the database:
[Authorize(Roles = "Admin,Order")]
[NotAuthorized(Roles = "Readonly")]
public async Task<IHttpActionResult> Delete(int orderid) {
var order = await _repository.Get(orderid);
if(order != null) {
await _repository.Delete(orderid);
return NoContent();
}
else {
return NotFound();
}
}

How to set ASP.NET Web Api Route Constraint HTTP Status Return Code

I am using ASP.NET Web Api 2 framework and using a basic route constraint as below.
[Route("Number/{id:int:min(2):max(10)}")]
public HttpResponseMessage GetNumber([FromUri] int id)
{
return (id > 0)
? Request.CreateResponse(HttpStatusCode.OK, id)
: Request.CreateResponse(HttpStatusCode.PreconditionFailed);
}
I would like to know when the id is conflict with the constraint above, .e.g. 1 or 11,
how can I overrride the default HTTP Status Return code which is 404?
Thanks very much.
You can create a custom route constraint that will check the min, max values like you wish, and additionally allow you to specify the HttpStatusCode in case the constraint is not fulfilled correctly.
public class RangeWithStatusRouteConstraint : IHttpRouteConstraint
{
private readonly int _from;
private readonly int _to;
private readonly HttpStatusCode _statusCode;
public RangeWithStatusRouteConstraint(int from, int to, string statusCode)
{
_from = from;
_to = to;
if (!Enum.TryParse(statusCode, true, out _statusCode))
{
_statusCode = HttpStatusCode.NotFound;
}
}
public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values,
HttpRouteDirection routeDirection)
{
object value;
if (values.TryGetValue(parameterName, out value) && value != null)
{
var stringValue = value as string;
var intValue = 0;
if (stringValue != null && int.TryParse(stringValue, out intValue))
{
if (intValue >= _from && intValue <= _to)
{
return true;
}
//only throw if we had the expected type of value
//but it fell out of range
throw new HttpResponseException(_statusCode);
}
}
return false;
}
}
Now you need to register it in the attribute mapping:
var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("rangeWithStatus", typeof(RangeWithStatusRouteConstraint));
config.MapHttpAttributeRoutes(constraintResolver);
And your route can now look like this:
public class MyController : ApiController
{
[Route("Number/{id:int:rangeWithStatus(2, 10, PreconditionFailed)}")]
public HttpResponseMessage GetNumber([FromUri] int id)
{
return Request.CreateResponse(HttpStatusCode.OK, id);
}
}
in this case Conflict is a string representation of HttpStatusCode.Conflict enumeration, which is later cast to the enum value in the constraint.
With such setup, if the value falls out of the [2, 10] range, the Web API infrastructure will respond with 409 status code instead of the default 404.
Remove the constraint on the route, and do the validation inside the method.
Constraints are used to figure out if a route should be used at all, but you want to use the route to be hit but change the status code if the input falls outside some parameters.
[Route("Number/{id})] // No constraints
public HttpResponseMessage GetNumber([FromUri] int id)
{
return (id > 1 && id < 11) // here you validate id range
? Request.CreateResponse(HttpStatusCode.OK, id)
: Request.CreateResponse(HttpStatusCode.PreconditionFailed);
}
I think that the correct solution is to check constraint inside controller (as #aanund suggested). However if you want to keep the Route constraints and avoid conditionals in your code, you may create another action controller with the same route but without constraints. If the constraints are not verified the new action controller will be called:
[Route("Number/{id:int:min(2):max(10)}")]
public HttpResponseMessage GetNumber([FromUri] int id)
{
Request.CreateResponse(HttpStatusCode.OK, id)
}
[Route("Number/{id}")]
public HttpResponseMessage GetNumber2([FromUri] int id)
{
Request.CreateResponse(HttpStatusCode.PreconditionFailed);
}

Trying to save comma-separated list

Trying to save selections from a CheckBoxList as a comma-separated list (string) in DB (one or more choices selected). I am using a proxy in order to save as a string because otherwise I'd have to create separate tables in the DB for a relation - the work is not worth it for this simple scenario and I was hoping that I could just convert it to a string and avoid that.
The CheckBoxList uses an enum for it's choices:
public enum Selection
{
Selection1,
Selection2,
Selection3
}
Not to be convoluted, but I use [Display(Name="Choice 1")] and an extension class to display something friendly on the UI. Not sure if I can save that string instead of just the enum, although I think if I save as enum it's not a big deal for me to "display" the friendly string on UI on some confirmation page.
This is the "Record" class that saves a string in the DB:
public virtual string MyCheckBox { get; set; }
This is the "Proxy", which is some sample I found but not directly dealing with enum, and which uses IEnumerable<string> (or should it be IEnumerable<Selection>?):
public IEnumerable<string> MyCheckBox
{
get
{
if (String.IsNullOrWhiteSpace(Record.MyCheckBox)) return new string[] { };
return Record
.MyCheckBox
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(r => r.Trim())
.Where(r => !String.IsNullOrEmpty(r));
}
set
{
Record.MyCheckBox = value == null ? null : String.Join(",", value);
}
}
To save in the DB, I am trying to do this in a create class:
proxy.MyCheckBox = record.MyCheckBox; //getting error here
but am getting the error:
Cannot implicitly convert 'string' to System.Collections.Generic.IEnumerable'
I don't know, if it's possible or better, to use Parse or ToString from the API for enum values.
I know that doing something like this will store whatever I put in the ("") into the DB, so it's just a matter of figuring out how to overcome the error (or, if there is an alternative):
proxy.MyCheckBox = new[] {"foo", "bar"};
I am not good with this stuff and have just been digging and digging to come up with a solution. Any help is much appreciated.
You can accomplish this using a custom user type. The example below uses an ISet<string> on the class and stores the values as a delimited string.
[Serializable]
public class CommaDelimitedSet : IUserType
{
const string delimiter = ",";
#region IUserType Members
public new bool Equals(object x, object y)
{
if (ReferenceEquals(x, y))
{
return true;
}
var xSet = x as ISet<string>;
var ySet = y as ISet<string>;
if (xSet == null || ySet == null)
{
return false;
}
// compare set contents
return xSet.Except(ySet).Count() == 0 && ySet.Except(xSet).Count() == 0;
}
public int GetHashCode(object x)
{
return x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var outValue = NHibernateUtil.String.NullSafeGet(rs, names[0]) as string;
if (string.IsNullOrEmpty(outValue))
{
return new HashSet<string>();
}
else
{
var splitArray = outValue.Split(new[] {Delimiter}, StringSplitOptions.RemoveEmptyEntries);
return new HashSet<string>(splitArray);
}
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
var inValue = value as ISet<string>;
object setValue = inValue == null ? null : string.Join(Delimiter, inValue);
NHibernateUtil.String.NullSafeSet(cmd, setValue, index);
}
public object DeepCopy(object value)
{
// return new ISet so that Equals can work
// see http://www.mail-archive.com/nhusers#googlegroups.com/msg11054.html
var set = value as ISet<string>;
if (set == null)
{
return null;
}
return new HashSet<string>(set);
}
public object Replace(object original, object target, object owner)
{
return original;
}
public object Assemble(object cached, object owner)
{
return DeepCopy(cached);
}
public object Disassemble(object value)
{
return DeepCopy(value);
}
public SqlType[] SqlTypes
{
get { return new[] {new SqlType(DbType.String)}; }
}
public Type ReturnedType
{
get { return typeof(ISet<string>); }
}
public bool IsMutable
{
get { return false; }
}
#endregion
}
Usage in mapping file:
Map(x => x.CheckboxValues.CustomType<CommaDelimitedSet>();

Resources