Web Api Post error -> Value cannot be null. Parameter name: uriString - post

I am relatively new to Web Api and I am having trouble POSTing a Person object. If I run in debug, I see that my uriString never gets set and I don't understand why. Because of this, I get "400 Bad Request" errors in Fiddler for all attempted Posts.
I have tried replicating what others have done when it comes to the Post action. Every example I've found uses a repository to add the person to the database. I do not have repositories however, but instead am using the NHibernate Save method to carry out this functionality. Here are the domain class, mapping by code file, WebApiConfig, and the PersonController.
public class Person
{
public Person() { }
[Required]
public virtual string Initials { get; set; }
public virtual string FirstName { get; set; }
public virtual char MiddleInitial { get; set; }
public virtual string LastName { get; set; }
}
public class PersonMap : ClassMapping<Person>
{
public PersonMap()
{
Table("PERSON");
Lazy(false);
Id(x => x.Initials, map => map.Column("INITIALS"));
Property(x => x.FirstName, map => map.Column("FIRST_NAME"));
Property(x => x.MiddleInitial, map => map.Column("MID_INITIAL"));
Property(x => x.LastName, map => map.Column("LAST_NAME"));
}
}
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var json = config.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects;
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Services.Replace(typeof(IHttpActionSelector), new HybridActionSelector());
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}/{action}/{actionid}/{subaction}/{subactionid}",
defaults: new { id = RouteParameter.Optional, action = RouteParameter.Optional,
actionid = RouteParameter.Optional, subaction = RouteParameter.Optional, subactionid = RouteParameter.Optional }
);
config.BindParameter( typeof( IPrincipal ), new ApiPrincipalModelBinder() );
// Uncomment the following line of code to enable query support for actions with an IQueryable or IQueryable<T> return type.
// To avoid processing unexpected or malicious queries, use the validation settings on QueryableAttribute to validate incoming queries.
// For more information, visit http://go.microsoft.com/fwlink/?LinkId=279712.
//config.EnableQuerySupport();
// To disable tracing in your application, please comment out or remove the following line of code
// For more information, refer to: http://www.asp.net/web-api
config.EnableSystemDiagnosticsTracing();
}
}
public class PersonsController : ApiController
{
private readonly ISessionFactory _sessionFactory;
public PersonsController (ISessionFactory sessionFactory)
{
_sessionFactory = sessionFactory;
}
// POST api/persons
[HttpPost]
public HttpResponseMessage Post(Person person)
{
var session = _sessionFactory.GetCurrentSession();
using (var tx = session.BeginTransaction())
{
try
{
if (!ModelState.IsValid)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
var result = session.Save(person);
var response = Request.CreateResponse<Person>(HttpStatusCode.Created, person);
string uriString = Url.Route("DefaultApi", new { id = person.Initials });
response.Headers.Location = new Uri(uriString);
tx.Commit();
return response;
}
catch (Exception)
{
tx.Rollback();
}
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
}
}
Fiddler information:
POST //localhost:60826/api/employees HTTP/1.1
Request Headers:
User-Agent: Fiddler
Content-Type: application/json
Host: localhost:xxxxx
Content-Length: 71
Request Body:
{
"Initials":"MMJ",
"LastName":"Jordan",
"FirstName":"Michael"
}
This line never sets the uriString to the correct value. string uriString = Url.Route("DefaultApi", new { id = person.Initials });
I've also tried using Url.Link instead of Url.Route. I've tried adding the controller = "Persons" inside the 'new' block, but that had no effect. Why isn't uriString being set? I'll listen to any thoughts at this point.
EDIT
I have tried
string uriString = Url.Link("DefaultApi", new { controller = "Persons", id = person.Initials, action="", actionid="", subaction="", subactionid="" });
as well as using a separate routeconfig
config.Routes.MapHttpRoute(
name: "PostApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional
} );
with
string uriString = Url.Link("PostApi", new { controller = "Persons", id = person.Initials});
and have had no luck.
SOLUTION
I was able to get this Post to work by using the line of code below. I'm not entirely sure if this is the correct way to do it, so if anybody knows differently, please share. Otherwise, I will happily use this approach.
response.Headers.Location = new Uri(this.Request.RequestUri.AbsoluteUri + "/" + person.Initials);

Problem seems to be here:
string uriString = Url.Route("DefaultApi", new { id = person.Initials });
You are only passing id while you need to be passing other parameters such as controller, etc.

You may construct URL this way:
string uriString = Url.Action("ActionName", "ControllerName", new { Id = person.Initials });

Related

How to configure Odata api for show result from table or stored procedure

I am going to create an Odata api in asp.net mvc 4 for get data from new table. when I call the Odata method and use debug in the code It shows me data properly. But when it comes to browser, it shows empty screen.
There is no error shown in the code.
this is my Odata method :
[Queryable]
public HCPData GetHCPData([FromODataUri] int key)
{
// return SingleResult.Create(db.HCPDatas.Where(hcpdata => hcpdata.Id == key));
IQueryable<HCPData> result = db.HCPDatas.Where(p => p.CompanyId == key);
return result.FirstOrDefault();
}
this is my WebApiConfig method:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
//var entitySetConfiguration1 = modelBuilder.EntitySet<Job>("Job");
var entitySetConfiguration1 = modelBuilder.EntitySet<HCPData>("HCPData");
var customer = modelBuilder.EntityType<HCPData>();
modelBuilder.EntitySet<HCPData>("HCPData");
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: null,
model: modelBuilder.GetEdmModel());
}
When I checked the console of empty screen in browser it shows an error: "NetworkError: 406 Not Acceptable - http://localhost:50369/HCPData?key=11"
Please let me know the solution of the issue. Thanks in advance.
What's the result if you change the controller as follows:
public class HCPDataController : ODataController
{
[EnableQuery]
public HCPData GetHCPData([FromODataUri] int key)
{
...
}
}
[My Sample]
Because, at my side, if I implement the controller as follows, it can work:
[EnableQuery]
public HCPData GetHCPData([FromODataUri] int key)
{
var data = new HCPData
{
CompanyId = 2,
Name = "Key = " + key
};
return data;
}
Example:
Let me issue the following request:
I can get the following response:
{
"#odata.context":"http://localhost:62591/odata/$metadata#HCPData/$entity","CompanyId":2,"Name":"Key = 11"
}

Troubleshoot MVC model binding failure - argument is null in controller

I am trying to POST an object from a WebJob to an MVC 4 controller. I am using Entity Framework. In the controller, I cannot get the object to bind properly (the argument is null). I have looked at many tutorials and it seems like my code should work.
Model (does this need to be in a specific namespace for EF to find it?):
public class CreateListingObject
{
public Listing listing;
public List<GalleryImage> images;
public CreateListingObject()
{
listing = new Listing();
images = new List<GalleryImage>();
}
}
public struct GalleryImage
{
public string picURL;
public string caption;
}
POST:
public void PostListing(CreateListingObject o)
{
Console.WriteLine("Posting listing: {0}", o.listing.Title);
HttpClient _httpClient = new HttpClient();
Uri uri = new Uri(_serviceUri, "/Automaton/CreateTestListing");
string json = BizbotHelper.SerializeJson(o);
HttpResponseMessage response = BizbotHelper.SendRequest(_httpClient, HttpMethod.Post, uri, json);
string r = response.Content.ReadAsStringAsync().Result;
response.EnsureSuccessStatusCode();
}
SendRequest (thank you Azure search samples):
public static HttpResponseMessage SendRequest(HttpClient client, HttpMethod method, Uri uri, string json = null)
{
UriBuilder builder = new UriBuilder(uri);
//string separator = string.IsNullOrWhiteSpace(builder.Query) ? string.Empty : "&";
//builder.Query = builder.Query.TrimStart('?') + separator + ApiVersionString;
var request = new HttpRequestMessage(method, builder.Uri);
if (json != null)
{
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
return client.SendAsync(request).Result;
}
Controller Action fragment (o is an empty object here):
[HttpPost]
public ActionResult CreateTestListing(CreateListingObject o)
{
Listing li = o.listing;
I have confirmed that if I post a simple object using the same code, everything works as expected.
Instead of sending a CreateListingObject in PostListing, I send this instead:
var test = new
{
data = "hi mom"
};
And change my action to, then the argument gets bound and I get valid data:
[HttpPost]
public ActionResult CreateTestListing(string data)
{
I have also checked the serialization of my CreateListingObject in the WebJob, and it is fully populated as I expect. This leads me to suspect that I am falling afoul of the default ModelBinder.

WebApi POST request gets handled by GET

I'm using RestSharp to consume my WebApi. Here is the relevant code:
var insertRequest = new RestRequest("MappedSystem", Method.POST);
insertRequest.AddBody(new MappedSystemCreateModel
{
MappedSystemDetails = new MappedSystemCreateModel.Details
{
SystemName = "TestName",
SystemVersion = "TV"
}
});
var response = RestClient.Execute(insertRequest);
But when I debug my WebApi it hits the Get() method:
public class MappedSystemController : ApiController
{
private readonly IMappedSystemService _mappedSystemService;
public MappedSystemController(IMappedSystemService mappedSystemService)
{
_mappedSystemService = mappedSystemService;
}
public MappedSystemViewModel[] Get()
{
=> return _mappedSystemService.Get();
}
public MappedSystemViewModel Get(Guid id)
{
return _mappedSystemService.Get(id);
}
[HttpPost]
public MappedSystemViewModel Post([FromBody]MappedSystemCreateModel model)
{
return _mappedSystemService.Post(model);
}
}
I think there must be something wrong with my routeConfig, but I don't know at this point:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}/{id2}/",
defaults: new { id = RouteParameter.Optional, id2 = RouteParameter.Optional }
);
There was a Post route handler interceptor checking to see if I had correctly added a trailing '/'. Since I hadn't it responded with a 301 and redirected. Somehow/somewhere in the RedirectPermanently() the Verb was getting lost. The answer was to originally make the request with the trailing '/'. But of course this does shed light on the error found in the redirect.
Also, I needed this following code on the request or the body wouldn't deserialize:
insertRequest.RequestFormat = DataFormat.Json;

Entity Framework outputs XML instead of JSON

In my controller classes I have a lot of methods which output properly formatted JSON data. All of them perform a database query.
E.g.
// GET api/User
public IEnumerable<Object> GetUsers()
{
var query = from user in db.Users
orderby user.Name
select new { Name = user.Name};
return query.AsEnumerable<Object>();
}
Now I'd like to return the properties of one of my classes.
public IEnumerable<Object> GetProperties()
{
return typeof(MyClass)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name).ToList();
}
However, instead of outputing JSON Entity Framework gives me some XML like
<ArrayOfstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<string>Property1</string>
<string>Property2</string>
</ArrayOfstring>
I've tried to convert the output in the controller method.
public string GetProperties()
{
var results = typeof(ComplexInfo)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name).ToList();
return JsonConvert.SerializeObject(results, Formatting.Indented,
new JsonSerializerSettings {
PreserveReferencesHandling = PreserveReferencesHandling.Objects
});
}
Unfortunately it's still outputting XML.
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">[
"Property1",
"Property2",
]</string>
How can I make sure to retrieve JSON objects from the controller?
I had also faced same issue in my web api. solution is, you have to remove XmlFormatter from WebApiConfig.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.
FirstOrDefault(t => t.MediaType == "application/xml");
config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
}
}

stackoverflow URL rewrite

How does SO perform the URL rewrite if we only put in the question ID?
questions/{id}/{whatever}
to
questions/{id}/{question-slug}
I've been working for some time with MVC and I have it working (routes, action, everything) so that it picks up the right content based on the provided ID.
However, the part after the {id} (the slug part) stays the same as typed in. So if someone typed in content/5/foobar it will display the right content but will leave the foobar in there.
In the controller (or somewhere else, please suggest where) I would need to go into the DB and pull out the right slug, put it in the route data and then perform a redirect to the same action with this correct data, I guess?
This is a try with Execute Result override. It works but does not redirect or replace/display the correct URL in browser:
protected override void Execute(System.Web.Routing.RequestContext requestContext) {
if (requestContext.RouteData.Values["id"] != null) {
string currentSlug = _repository.Find(int.Parse(requestContext.RouteData.Values["id"].ToString())).Slug;
if (requestContext.RouteData.Values["slug"] != null) {
requestContext.RouteData.Values.Remove("slug");
}
requestContext.RouteData.Values.Add("slug", currentSlug);
}
base.Execute(requestContext);
}
This is another, nicely working, version of a Display action, so you can see what it does and get an idea what I want:
//
// GET: {culture}/directory/5/{slug}
public virtual ActionResult Display(int id, string slug)
{
var model = _repository.Find(id);
if (model != null) {
if (!model.Slug.Equals(slug, System.StringComparison.OrdinalIgnoreCase)) {
return RedirectToActionPermanent(pndng.DirectoryEntry.ActionNames.Display, pndng.DirectoryEntry.Name, new { id = model.Id, slug = model.Slug });
}
return View(model);
}
// no model found
return InvokeHttp404(HttpContext);
}
This one performs permanent redirect (it does what I want) but is it right?
I guess I need a redirect to refresh the browser URL, don't I?
public ActionResult Details(int id, string slug)
{
var session = MvcApplication.CurrentRavenSession;
var blogPostRelations = session
.Query<BlogPost, BlogPosts_WithRelatedData>()
.Where(x => x.IntId == id)
.As<BlogPostRelations>()
.FirstOrDefault()
;
if (blogPostRelations == null)
return HttpNotFound();
if (blogPostRelations.BlogPost.DisplayData.Slug.Value != slug)
return RedirectToActionPermanent("Details", new { id = id, slug = blogPostRelations.BlogPost.DisplayData.Slug.Value });
return View(blogPostRelations);
}
Notice the:
if (blogPostRelations.BlogPost.DisplayData.Slug.Value != slug)
return RedirectToActionPermanent("Details", new { id = id, slug = blogPostRelations.BlogPost.DisplayData.Slug.Value });
So your #2 approach is the right one.
You could write a custom route for this:
public class QuestionsRoute : Route
{
public QuestionsRoute()
: base(
"questions/{id}/{slug}",
new RouteValueDictionary(new
{
controller = "questions",
action = "index",
slug = UrlParameter.Optional
}),
new RouteValueDictionary(new
{
id = #"\d+"
}),
new MvcRouteHandler()
)
{ }
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
var id = rd.GetRequiredString("id");
var slug = rd.Values["slug"] as string;
if (string.IsNullOrEmpty(slug))
{
slug = GoFetchSlugFromDb(id);
if (string.IsNullOrEmpty(slug))
{
return null;
}
httpContext.Response.RedirectToRoutePermanent(new
{
action = "index",
controller = "questions",
id = id,
slug = slug
});
return null;
}
return rd;
}
private string GoFetchSlugFromDb(string id)
{
// TODO: you know what to do here
throw new NotImplementedException();
}
}
which will be registered in Application_Start:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("questions", new QuestionsRoute());
}
Now your QuestionsController will be pretty simple:
public class QuestionsController: Controller
{
public ActionResult Index(int id, string slug)
{
...
}
}

Resources