I'm trying to understand how to properly implement a microservice pattern with REST endpoints.
I understand the very basics of it. The internet is full of that. Typical example:
class User
{
public User GetUser(int id) { ... }
public User GetUsers() { ... }
public User PutUser(User user) { ... }
public User PostUser(User user) { ... }
}
So if I want one User then I'd do a GET request to GetUser(100), and it would return a User object as JSON.
But suppose I have a page where I want to list the firstname and lastname of all users. Then I could do a GET request to GetUsers(). But if the User table has 100 columns in the Database and I only need to display two columns (firstname, lastname), then it's overkill to get the other 98 columns along with it.
Maybe on another page I need to display 40 of the 100 columns. And on some other page 20 of the 100 columns.
So that means I need 3 extra end points that all return all users. But each end point should return different data.
How would I name those new endpoints?
Do I have to call those 3 endpoints explicitly by name? (Ex: GetUsersNames() GetUsersAge() etc?)
PS. This is probably a poor example, but I hope you understand what I'm getting at. I don't know what to do / how to name the endpoints when I go beyond the default GET/PUT/POST/DELETE methods.
What you can do is to ask for the particular fields explicitly, so:
GET /api/users/?fields=firstName,lastName
This is much better than introducing new endpoints.
You can define custom views by using Attribute Routing. The methods you have in your controller default to the route pattern that you define in your configuration.
You can however explicitely define additional routes. You need to include this in your Configuration:
// this enables route attributes (route annotations at the actions in the controller)
config.MapHttpAttributeRoutes();
Then you can define additional routes in your controller and add the required filter logic:
[Route("api/users/range/{rangeFrom}/{rangeTo}")]
[HttpGet]
public IHttpActionResult GetUsers(int from, int to)
{
return Ok(Users.Skip(from).Take(to-from));
}
[Route("api/users/namesonly")]
[HttpGet]
public IHttpActionResult GetUsers()
{
return Ok(Users.Select(u => new { firstName = u.FirstName, lastName = u.LastName }));
}
Reference: Attribute Routing in ASP.NET Web API 2
EDIT:
Yes, not all options should be resolved in separate endpoints, but it's an option at a certain point. What others do: Use URI parameters to limit returned data count or set flags, and have them available through the method's signature e.g.:
[Route("api/users/")]
[HttpGet]
public IHttpActionResult GetUsers([FromUri]int? from, [FromUri]int? count)
{
return Ok(Users.Skip(from ?? 0).Take(count ?? Users.Count));
}
Or to have shorter user objects returned:
[Route("api/users/")]
[HttpGet]
public IHttpActionResult GetUsers([FromUri]bool? shortVersion)
{
if (shortVersion.HasValue && shortVersion.Value)
return Ok(Users.Select(u => new { firstName = u.FirstName, lastName = u.LastName }));
else
return Ok(Users);
}
Related
I'm looking for a way to intercept Entity Framework's lazy load proxy implementation, or otherwise control what is returned when accessing a Navigation property that may have no value in the database.
An example of what I'm looking for is this Contact class with mailing address, business phone, etc. that may or may not have a contact person.
public partial class Contact
{
private Nullable<System.Guid> _personId;
public Nullable<System.Guid> PersonId
{
get { return _personId; }
set { SetProperty(ref _personId, value); }
}
public virtual Person Person{ get; set; }
// mailing address, other properties...
}
public partial class Person
{
private string _firstName;
public string FirstName
{
get { return _firstName; }
set { SetProperty(ref _firstName, value); }
}
private string _lastName;
public string LastName
{
get { return _lastName;}
set { SetProperty(ref _lastName;value); }
}
}
It is very useful in ASP.net Razor pages, WPF or ad-hoc reporting tools, to be able to use expressions like:
Contact c = repo.GetContact(id);
Console.WriteLine("Contact Person " + c.Person.FirstName);
Which of course fails if there is no PersonId, and hence contact.Person is null.
Tools like Ideablade Devforce have a mechanism to return a "NullEntity" for Person in this case, which allows the WriteLine to succeed. Additionally, the NullEntity for Person can be configured to have a sensible value for FirstName, like "NA".
Is there some way to override the Dynamic Proxy mechanism in EF, or otherwise intercept the reference to Person from Contact to enable this scenario?
I have investigated IDbCommandInterceptor, but that does not seem to intercept virtual navigation to individual entity properties, only navigation to entity collections.
Update _____________________________________
To elaborate on my original question, I can't modify the expression by introducing null conditional operators into the them, as these expressions are incorporated into WPF, ASP.Net Razor binding expressions, and/or report data fields, created by other developers or authors. Also, there may be multiple layers of null properties to deal with, e.g. Contact.Person.Spouse.FirstName, where either Person and/or Spouse might be a "null" property. The Devforce Ideablade implementation deals with this perfectly, but is unfortunately not an option on my current project.
you can use a null-conditional operator from c# like this
c.Person?.FirstName
This means that when Person == null , return null or otherwise return FirstName. You would still need to handle the null value
See : https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-
I am using ASP.NET Web API. I want to REST uri to be
GET /api/v1/documents/1234/download or
GET /api/v1/documents/1234?act=download or
GET /api/v1/documents?id=1234&act=download
Is it possible to have multiple ways to call REST API Url? Is it recommended?
I am using Attribute Routes only
[RoutePrefix("api/v1")]
public class DocumentController : ApiController
{
private readonly DomainService _domainService;
public DocumentController(DomainService domainService)
: base(domainService)
{
_domainService = domainService ?? throw new ArgumentNullException(nameof(domainService));
}
[HttpGet]
[Route("documents/{id:int}")]
public async Task<IHttpActionResult> DownloadDocument([FromUri]int id, [FromUri]string act)
{
if (string.IsNullOrEmpty(act) || act.ToUpper() != "DOWNLOAD")
{
return BadRequest("Invalid action parameter.");
}
return await service.DownloadFile(id);
}
}
with above code only GET /api/v1/documents/1234?act=download works. Is it possible to configure route in a such way that all 3 routes will invoke same action method?
You can add as many Route attributes as required to each method.
So you could do this to your method:
[Route("documents")] // matches /documents?id=123&act=download
[Route("documents/{id:int}")] // matches /documents/123?act=download
[Route("documents/{id:int}/{act}")] // matches /documents/123/download
Personally I think this is quite long-winded, and would try to stick to a single style (the last one if I could choose), but I guess it could depend on your requirements.
When exposing querystring parameters using GET I have the following base URL:
https://school.service.com/api/students
This will return the first 25 students.
What if I want to return a list of students based on ONE of the following criteria:
* have accepted a job
* have received a job offer
* have no job offers
The three above choices are essentially an enum.
Therefore, the query request for students who have no job offers I assume would look like:
https://school.service.com/api/students?jobOfferStatus=3
However, I'm wondering if jobOfferStatus=3 is the proper way to handle this. If so, how would I publish/provide to the clients a list of available options for that jobOfferStatus query parameter? What about other possible query parameters and their valid options? We'll have many possible query parameters like this.
I'd love to see an example of how this should be done properly. What are the best practices?
There are two main options: documenting it, or making it discoverable. A lot of APIs have documentation where they list all of the resources and parameters for reference. Otherwise, the client won't know.
You could also make it discoverable in some way by including the options in a response. For conventions on this, search for HATEOAS if you haven't already. (I'm not really knowledgeable enough about HATEOAS myself to make a suggestion.)
I will mention that "3" is not a very meaningful value for jobOfferStatus, and there's no need for the client to know that number. You can make it anything you want -- jobOfferStatus=none or even jobOffer=none. Your controller can do the work of matching that value to your enumeration. Try to design your interface to be intuitive for developers (and, of course, write good documentation).
To handle multiple query parameters, you can use optional parameters in your function:
public HttpResponseMessage GetStudents(string jobOffer = "",
string other1 = "",
string other2 = "")
{
if (jobOffer == "accepted" && other2 == "whatever") {
// return a response
}
else {
// return a different response
}
}
When the client uses parameters by those names, you can tailor your response appropriately.
You have some options to do this, let's try to help:
1) Configure a generic route to asp.net web api knows how to solve another action's name different from Get to a get method, on the App_Start\WebConfigApi.cs class, try to add this:
config.Routes.MapHttpRoute("DefaultApiWithActionAndId",
"api/{controller}/{action}/{id}",
new { id = RouteParameter.Optional });
Using it, you can have diferent methods on the api controller:
// request: get
// url: api/Students/GetStudents
public HttpResponseMessage GetStudents()
{
return Request.CreateResponse(...);
}
// request: get
// url: api/Students/GetStudentsWithJobOffer
public HttpResponseMessage GetStudentsWithJobOffer()
{
return Request.CreateResponse(...);
}
// request: get
// url: api/Students/GetStudentsAcceptedJob
public HttpResponseMessage GetStudentsAcceptedJob()
{
return Request.CreateResponse(...);
}
2) Use a simple parameter on the Get method:
// request: get
// url: api/Students?jobOfferStatus=1
public HttpResponseMessage GetStudents(int jobOfferStatus)
{
// use jobOfferStatus parameter to fill some list
return Request.CreateResponse(...);
}
3) Use a simple method with a parameter named id, to get a default friendly url by asp.net mvc web api.
// request: get
// url: api/Students/1
public HttpResponseMessage GetStudents(int id)
{
// use the id parameter to fill some list
return Request.CreateResponse(...);
}
I have written a REST service using Web API and after reading sections of this Web API Design from Brian Mulloy, was trying to figure out how I could implement associations with Web API.
Web API Design Extract:
Associations
Resources almost always have relationships to other
resources. What's a simple way to express these relationships in
aWebAPI?
Let's look again at the API we modeled in nouns are good,
verbs are bad -theAPI that interacts with our dogs resource.
Remember, we had two base URLs: /dogs and dogs/1234.
We're using HTTP
verbs to operate on the resources and collections. Our dogs belong to
owners. To get all the dogs belonging to a specific owner, or to
create a new dog for that owner, do a GET or a POST:
GET /owners/5678/dogs
POST /owners/5678/dogs
Now, the relationships can be
complex. Owners have relationships with veterinarians, who have
relationships with dogs, who have relationships with food, and so on.
It's not uncommon to see people string these together making a URL 5
or 6 levels deep. Remember that once you have the primary key for one
level, you usually don't need to include the levels above because
you've already got your specific object. In other words, you shouldn't
need too many cases where a URL is deeper than what we have above
/resource/identifier/resource.
So I tried to add a controller method for the association like follows:
public class EventsController : ApiController
{
// GET api/events
public IEnumerable<Event> Get()
{
// get list code
}
// GET api/events/5
public Event Get(int id)
{
// get code
}
// POST api/events
public void Post([FromBody]Event evnt)
{
// add code
}
// POST api/events/5
public void Post(int id, [FromBody]Event evnt)
{
// update code
}
// DELETE api/events/5
public void Delete(int id)
{
// delete code
}
// GET api/events/5/guests
public IEnumerable<Guest> Guests(int id)
{
// association code
}
}
I also modified my route templates to the following:
config.Routes.MapHttpRoute("ApiWithAssociations",
"api/{controller}/{id}/{action}");
config.Routes.MapHttpRoute("DefaultApi",
"api/{controller}/{id}",
new { id = RouteParameter.Optional });
Unfortunately, when I do an update/post of the event resource I now get a HTTP 500 Internal Server Error with a response body stating
Multiple actions were found that match the request
I've tried modifying the route templates in conjunction with adding System.Web.Http.HttpPostAttribute (and other HTTP verbs) as well but to no avail.
Has anyone tried this and got it working? Any help would be appreciated. If it is absolutely not possible to have multiples for an http verb then I guess I'll have to abandon associations with my REST service.
EDIT: SOLUTION
Using Radim Köhler's answer, I was able to get this working. Add the HttpGetAttribute to the Guests method like so:
// GET api/event/5/guests
[HttpGet]
public IEnumerable<Guest> Guests(int id)
{
// association code
}
And added an addition route to cater for the default GET action like follows:
config.Routes.MapHttpRoute("DefaultGet",
"api/{controller}/{id}",
new {action = "Get"},
new {httpMethod = new HttpMethodConstraint(HttpMethod.Get)});
config.Routes.MapHttpRoute("ApiWithAssociations",
"api/{controller}/{id}/{action}");
config.Routes.MapHttpRoute("DefaultApi",
"api/{controller}/{id}",
new {id = RouteParameter.Optional});
The solution, could be in an explicit POST mapping
Just add new definition, which will be used for events/5 POST
// explicit Post() mapping
config.Routes.MapHttpRoute(
name: "DefaultPost",
routeTemplate: "api/{controller}/{id}",
defaults: new { action = "Post" }
, constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Post) }
);
// existing
config.Routes.MapHttpRoute("ApiWithAssociations",
"api/{controller}/{id}/{action}");
config.Routes.MapHttpRoute("DefaultApi",
"api/{controller}/{id}",
new { id = RouteParameter.Optional });
As it is not, I have a site where you must come in on a single url and a cookie is set to track which customer you are affiliated with. I want to change this so that certain controllers only use a url like this:
/{friendlyName}/{controller}/{index}/{id}
that friendly name is unique and lets me select the correct customer without using the cookie kludge.
I have controllers: Home, Redirect that I do not want the friendly name part of (and possibly more).
I have a few others that fit this category that I would like to move into their own areas. How can I not include the areas as valid friendly names? For instance, I have a controller that services up content in an iframe called Framed. currently, a url for this looks like /Framed/action/id. I could put this in an area called Framed with a controller the same name as the action, and I should still be able to maintain the same url.
For the controller Error I want the friendly name to be optional
I have other controllers that I want the friendly name to be required: SignIn, SignOut, Account
Once I have the routing, the problem is altering the code so that my redirects maintain the friendlyurl. Any ideas on how to do that?
My problem is just coming up with a good plan of attack on how to change the routing of my site. I must maintain backwards compatibility of some of the urls - namely anything I don't want the friendly url part of, including the controllers I discussed slitting into their own areas. I'm looking for any good suggests on how to lay this out and go about altering the changes.
To accomplish your objectives, you will need a combination of routes and RouteConstraints. Also, you will need to enforce rules that a friendlyName is unique, and is different from the names of any controllers or areas.
The following routes should be sufficient in RegisterRoutes() in Global.asax.cs:
routes.MapRoute(
"WithFriendlyName",
"{friendlyName}/{controller}/{index}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { friendlyName = new MustBeFriendlyName() }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = new MustNotRequireFriendlyName() }
);
The RouteConstraints should look something like this:
using System;
using System.Web;
using System.Web.Routing;
namespace Examples.Extensions
{
public class MustBeFriendlyName : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// return true if this is a valid friendlyName
// MUST BE CERTAIN friendlyName DOES NOT MATCH ANY
// CONTROLLER NAMES OR AREA NAMES
var _db = new DbContext();
return _db.FriendlyNames.FirstOrDefault(x => x.FriendlyName.ToLowerInvariant() ==
values[parameterName].ToString().ToLowerInvariant()) != null;
}
}
public class MustNotRequireFriendlyName : IRouteConstraint
{
private const string controllersRequiringFriendlyNames =
"SignIn~SignOut~Account";
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// return true if this controller does NOT require a friendlyName
return controllersRequiringFriendlyNames.ToLowerInvariant()
.Contains(values[parameterName].ToString().ToLowerInvariant());
}
}
}
This should get you started.
As far as the URLs generated by your redirects, if the routing is set up correctly, the URL generation should follow, so that the only changes you are likely to need are those to insure {friendlyName} is being passed.
You probably will have to add some additional routes and constraints as you get further into your changes.
Just wanted to add to this, as the optional prefix been biting me for the past couple of days. While I want to use the solution provided by #counsellorben, I also needed to be able to address the routes by the same name, which is impossible when using 2 routes.
It took me some headscratching, but finally the solution actually seemed very simple. I just needed to create an intermediate aggregate route:
public class AggregateRoute : RouteBase
{
private readonly RouteBase[] _routes;
public AggregateRoute(params RouteBase[] routes)
{
_routes = routes;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData routeData = null;
foreach (var route in _routes)
{
routeData = route.GetRouteData(httpContext);
if (routeData != null) break;
}
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData virtualPath = null;
foreach (var route in _routes)
{
virtualPath = route.GetVirtualPath(requestContext, values);
if (virtualPath != null) break;
}
return virtualPath;
}
}
This allows me to do:
routes.Add(
"RouteName",
new AggregateRoute(
new Route("{path}", new MvcRouteHandler()),
new Route("{prefix}/{path}", new MvcRouteHandler())
)
);
Which enables resolving either route by the same name, which is impossible when adding both routes separately:
Url.RouteLink("RouteName", new RouteValueDictionary{
new{path="some-path"}});
Url.RouteLink("RouteName", new RouteValueDictionary{
new{path="some-prefix/some-path"}});