I am trying to POST to my ServiceStack service and retrieve the Location header from the response of my CREATED entity. I am not sure whether using IReturn is valid but I am not sure how to access the Response headers from my client.
Can someone help me understand how to interact with the HttpResult properly? There is a test case at the bottom of the code to demonstrate what I want to do.
Here's the codz:
public class ServiceStackSpike
{
public class AppHost : AppHostHttpListenerBase
{
public AppHost() : base("TODOs Tests", typeof(Todo).Assembly) { }
public override void Configure(Container container)
{
//noop
}
}
[Route("/todos", "POST")]
public class Todo:IReturn<HttpResult>
{
public long Id { get; set; }
public string Content { get; set; }
public int Order { get; set; }
public bool Done { get; set; }
}
public class TodosService : Service
{
public object Post(Todo todo)
{
//do stuff here
var result = new HttpResult(todo,HttpStatusCode.Created);
result.Headers[HttpHeaders.Location] = "/tada";
return result;
}
}
public class NewApiTodosTests : IDisposable
{
const string BaseUri = "http://localhost:82/";
AppHost appHost;
public NewApiTodosTests()
{
appHost = new AppHost();
appHost.Init();
appHost.Start(BaseUri);
}
[Fact]
public void Run()
{
var restClient = new JsonServiceClient(BaseUri);
var todo = restClient.Post(new Todo { Content = "New TODO", Order = 1 });
Assert.Equal(todo.Headers[HttpHeaders.Location], "/tada"); //=>fail
}
public void Dispose()
{
appHost.Dispose();
appHost = null;
}
}
}
See the Customizing HTTP Responses ServiceStack wiki page for all the different ways of customizing the HTTP Response.
A HttpResult is just one way customize the HTTP Response. You generally want to include the Absolute Url if you're going to redirect it, e.g:
public object Post(Todo todo)
{
var todo = ...;
return new HttpResult(todo, HttpStatusCode.Created) {
Location = base.Request.AbsoluteUri.CombineWith("/tada")
};
}
Note HTTP Clients will never see a HttpResult DTO. HttpResult is not a DTO itself, it's only purpose is to capture and modify the customized HTTP Response you want.
All ServiceStack Clients will return is the HTTP Body, which in this case is the Todo Response DTO. The Location is indeed added to the HTTP Response headers, and to see the entire HTTP Response returned you should use a HTTP sniffer like Fiddler, WireShark or Chrome's WebInspector.
If you want to access it using ServiceStack's HTTP Clients, you will need to add a Response Filter that gives you access to the HttpWebResponse, e.g:
restClient.ResponseFilter = httpRes => {
Assert.Equal(httpRes.Headers[HttpHeaders.Location], "/tada");
};
Todo todo = restClient.Post(new Todo { Content = "New TODO", Order = 1 });
Inspecting Response Headers using Web Request Extensions
Another lightweight alternative if you just want to inspect the HTTP Response is to use ServiceStack's Convenient WebRequest extension methods, e.g:
var url = "http://path/to/service";
var json = url.GetJsonFromUrl(httpRes => {
Assert.Equal(httpRes.Headers[HttpHeaders.Location], "/tada");
});
Related
I have an implementation, where I am calling an autoquery operation via the service gateway. The service gateway will successfully call both internal and external operations. However, any autoquery operation fails because it is not getting the connection string set. These same autoquery operations work just fine when called directly and not through the gateway.
Here is the stack trace.
at ServiceStack.OrmLite.OrmLiteConnectionFactory.CreateDbConnection() in C:\\BuildAgent\\work\\27e4cc16641be8c0\\src\\ServiceStack.OrmLite\\OrmLiteConnectionFactory.cs:line 70\r\n at ServiceStack.OrmLite.OrmLiteConnectionFactory.OpenDbConnection() in C:\\BuildAgent\\work\\27e4cc16641be8c0\\src\\ServiceStack.OrmLite\\OrmLiteConnectionFactory.cs:line 95\r\n at ServiceStack.ServiceStackHost.GetDbConnection(IRequest req) in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack\\ServiceStackHost.Runtime.cs:line 691\r\n at ServiceStack.AutoQuery.GetDb(Type type, IRequest req) in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack.Server\\AutoQueryFeature.cs:line 598\r\n at ServiceStack.AutoQuery.CreateQuery[From](IQueryDb`1 dto, Dictionary`2 dynamicParams, IRequest req) in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack.Server\\AutoQueryFeature.cs:line 608\r\n at IDOE.SecurityPortal.Api.ServiceInterface.OrganizationUserStaffTypeService.Get(QueryOrganizationUserStaffTypes query) in E:\\source\\repos\\Azure - Security Portal\\src\\IDOE.SecurityPortal\\IDOE.SecurityPortal.Api.ServiceInterface\\OrganizationUserStaffTypeService.cs:line 47\r\n at ServiceStack.Host.ServiceRunner`1.<ExecuteAsync>d__15.MoveNext() in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack\\Host\\ServiceRunner.cs:line 133
Database Connection Registration in startup.cs
var dbFacotry = container.Resolve<IDbConnectionFactory>();
dbFacotry.RegisterConnection("SecPortal", AppSettings.Get<string>("SQLSERVER-SECPORTAL-CONNECTIONSTRING"), SqlServer2017Dialect.Provider);
dbFacotry.RegisterConnection("EdfiMdm", AppSettings.Get<string>("SQLSERVER-EDFIMDM-CONNECTIONSTRING"), SqlServer2017Dialect.Provider);
Plugins.Add(new AutoQueryFeature { IncludeTotal = true });
AutoQuery Definition
[Authenticate]
[RequiredClaim("scope", "secprtl-read")]
[Route("/files", Verbs = "GET")]
[ConnectionInfo(NamedConnection = "SecPortal")]
public class QueryFiles : QueryDb<Types.File>
{
[QueryDbField(Field = "Id", Template = "({Value} IS NULL OR {Field} = {Value})")]
public int? Id { get; set; }
[QueryDbField(Field = "FileName", Template = "({Value} IS NULL OR UPPER({Field}) LIKE UPPER({Value}))", ValueFormat = "%{0}%")]
public string FileName { get; set; }
[QueryDbField(Field = "UserId", Template = "({Value} IS NULL OR UPPER({Field}) LIKE UPPER({Value}))", ValueFormat = "%{0}%")]
public string UserId { get; set; }
[QueryDbField(Field = "StateOrganizationId", Template = "({Value} IS NULL OR UPPER({Field}) LIKE UPPER({Value}))", ValueFormat = "%{0}%")]
public string StateOrganizationId { get; set; }
[QueryDbField(Field = "Notes", Template = "({Value} IS NULL OR UPPER({Field}) LIKE UPPER({Value}))", ValueFormat = "%{0}%")]
public string Notes { get; set; }
}
Code calling the service
public class ContactService : Service
{
public ContactService()
{
}
public async Task<object> Post(PostContact request)
{
try
{
var files = base.Gateway.Send(new QueryFiles() { });
return new Contact() { Name = request.Name };
}
catch (Exception ex)
{
throw ex;
}
}
}
Custom Service Gateway
public class CustomServiceGatewayFactory : ServiceGatewayFactoryBase
{
private IRequest request;
public override IServiceGateway GetServiceGateway(IRequest request)
{
this.request = request;
return base.GetServiceGateway(request);
}
public override IServiceGateway GetGateway(Type requestType)
{
var isLocal = HostContext.Metadata.RequestTypes.Contains(requestType);
if (isLocal)
{
return base.localGateway;
}
else
{
return new JsonServiceClient("https://localhost:6001")
{
BearerToken = request.GetBearerToken()
};
}
}
}
Custom service gateway registration in startup.cs
container.Register<IServiceGatewayFactory>(x => new CustomServiceGatewayFactory()).ReusedWithin(ReuseScope.None);
The call being made in the service class is a local call. Calling an external service that uses autoquery works just fine. I can also call the local service directly with no problem.
I created a custom autoquery method in the service interface, I noticed that the db connection info was not populated on the request.items array. So I manually added that information to the request, and it worked as expected. So somehow, in my setup, the autoquery operations that are called locally, the db connection info is not getting added to the request object.
Request Filter Attributes like [ConnectionInfo] is only applied on HTTP Requests, not internal Service Gateway requests.
The Connection info isn't configured because it's not annotated on your PostContact Service Request DTO that calls the in-procces Service Gateway.
You can have the [ConnectionInfo] on the QueryFiles AutoQuery Request DTO attached to the current Request with:
public async Task<object> Post(PostContact request)
{
try
{
typeof(QueryFiles).FirstAttribute<ConnectionInfoAttribute>()
.Execute(Request,Response,request);
var files = base.Gateway.Send(new QueryFiles() { });
return new Contact() { Name = request.Name };
}
catch (Exception ex)
{
throw ex;
}
}
I'm working in ServiceStack and using FluentValidation to handle incoming DTOs on requests. I've broken these out as follows, but my unit tests don't seem to be able to target specific rule sets. My code is as follows:
DTO / REQUEST CLASSES
public class VendorDto
{
public int Id { get; set; }
public string Name { get; set; }
}
public class DvrDto
{
public int Id { get; set; }
public string Name { get; set; }
public VendorDto Vendor { get; set; }
}
public class DvrRequest : IReturn<DvrResponse>
{
public DvrDto Dvr { get; set; }
}
VALIDATOR CLASSES
public class VendorValidator : AbstractValidator<VendorDto>
{
public VendorValidator()
{
RuleFor(v => v.Name).NotEmpty();
}
}
public class DvrValidator : AbstractValidator<DvrDto>
{
public DvrValidator()
{
RuleFor(dvr => dvr.Name).NotEmpty();
RuleFor(dvr => dvr.Vendor).NotNull().SetValidator(new VendorValidator());
}
}
public class DvrRequestValidator : AbstractValidator<DvrRequest>
{
public DvrRequestValidator()
{
RuleSet(HttpMethods.Post, () =>
{
RuleFor(req => req.Dvr).SetValidator(new DvrValidator());
});
RuleSet(HttpMethods.Patch, () =>
{
RuleFor(req => req.Dvr).SetValidator(new DvrValidator());
RuleFor(req => req.Dvr.Id).GreaterThan(0);
});
}
}
UNIT TEST
[TestMethod]
public void FailWithNullDtoInRequest()
{
// Arrange
var dto = new DvrRequest();
var validator = new DvrRequestValidator();
// Act
var result = validator.Validate(msg, ruleSet: HttpMethods.Post);
// Assert
Assert.IsTrue(result.IsValid);
}
I would prefer to be able to control what gets called depending on what the HttpMethod is that's being called. My thought here was, I want to validate all fields on the DvrDto (and child VendorDto) for both POST and PATCH, but only require a valid Id be set on PATCH. I am setting up my DvrRequestValidator to handle this. However, my unit test as written above (targeting the RuleSet for the POST verb) always finds the request to be valid, even though the validator should fail the request.
In fiddling with it, if I make the following changes:
VALIDATOR
public class DvrRequestValidator : AbstractValidator<DvrRequest>
{
public DvrRequestValidator()
{
RuleFor(req => req.Dvr).SetValidator(new DvrValidator());
RuleSet(HttpMethods.Patch, () =>
{
RuleFor(req => req.Dvr.Id).GreaterThan(0);
});
}
}
TEST CALL (removing the targeted verb)
// Act
var result = validator.Validate(msg); // , ruleSet: HttpMethods.Post);
The validator then works as I expect for a POST, but the PATCH rule set doesn't get executed. As a result, I seem to lose the granularity of what I want validated on a particular verb. It would appear to me that this is supported in examples I've seen both on StackOverflow and in the FluentValidation docs. Am I doing something wrong here? Or is this not possible?
The Validators are registered in ServiceStack's Global Request Filters so you'd typically use an Integration Test with a Service Client to test validation errors.
If you want to test the validator independently in a Unit Test you can execute a HTTP Method Result set with something like:
var req = new BasicRequest(requestDto);
var validationResult = validator.Validate(new ValidationContext(requestDto, null,
new MultiRuleSetValidatorSelector(HttpMethods.Patch)) {
Request = req
});
Unit Testing ServiceStack Features
Although note a lot of ServiceStack functionality assumes there's an AppHost is available, but in most cases you can just use an In Memory AppHost, e.g:
[Test]
public void My_unit_test()
{
using (new BasicAppHost().Init())
{
//test ServiceStack classes
}
}
Of if you prefer you can set it up once per test fixture with something like:
public class MyUnitTests
{
ServiceStackHost appHost;
public MyUnitTests() => appHost = new BasicAppHost().Init();
[OneTimeTearDown]
public void OneTimeTearDown() => appHost.Dispose();
[Test]
public void My_unit_test()
{
//test ServiceStack classes
}
}
I know I can manage the routes for the REST-ful interface operations by attributing the DTOs
[Route("/widgets", "GET, POST")]
[DataContract()]
public class GetWidgetsRequest
{
[DataMember]
public string OutletCode { get; set; }
[DataMember]
public IList<Specification> WidgetsCaptured { get; set; }
}
but I have searched and experimented unsuccessfully at trying to affect the default /soap11 appendage to the endpoint for a given SOAP operation.
**POST /soap11 HTTP/1.1**
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: nnn
SOAPAction: GetItemsRequest
A broader question within the question is, what are my options and how to configure the different endpoint settings?
Thanks!
Please read the SOAP Support docs for guidelines and limitations of using SOAP in ServiceStack.
For a different SOAP path, eg ~/services, you can add your own servicestack plugin , that returns your own servicestack soap handler.
public class MySoapFeature : IPlugin
{
private static IHttpHandler GetHandlerForPathParts(string[] pathParts)
{
string str2 = string.Intern(pathParts[0].ToLower());
if (pathParts.Length != 1) return null;
if (str2 == "services")
{
return new MySoapHttpHandler();
}
return null;
}
public IHttpHandler ProcessRequest(string httpMethod, string pathInfo, string filePath)
{
char[] chrArray = new char[] { '/' };
string[] strArrays = pathInfo.TrimStart(chrArray).Split(new char[] { '/' });
if ((int)strArrays.Length == 0)
{
return null;
}
return MySoapFeature.GetHandlerForPathParts(strArrays);
}
public void Register(IAppHost appHost)
{
appHost.CatchAllHandlers.Add(this.ProcessRequest);
}
}
Then implement this handler based on Soap11Handler or Soap12Handler
public class MySoapHttpHandler : Soap11Handler, IHttpHandler
{
public MySoapHttpHandler()
: base((EndpointAttributes)((long)32768))
{
}
public new void ProcessRequest(HttpContext context)
{
if (context.Request.HttpMethod == "GET")
{
(new Soap11WsdlMetadataHandler()).Execute(context);
return;
}
Message message = base.Send(null);
context.Response.ContentType = base.GetSoapContentType(context.Request.ContentType);
using (XmlWriter xmlWriter = XmlWriter.Create(context.Response.OutputStream))
{
message.WriteMessage(xmlWriter);
}
}
public override void ProcessRequest(IHttpRequest httpReq, IHttpResponse httpRes, string operationName)
{
if (httpReq.HttpMethod == "GET")
{
(new Soap11WsdlMetadataHandler()).Execute(httpReq, httpRes);
return;
}
Message message = base.Send(null, httpReq, httpRes);
httpRes.ContentType = base.GetSoapContentType(httpReq.ContentType);
using (XmlWriter xmlWriter = XmlWriter.Create(httpRes.OutputStream))
{
message.WriteMessage(xmlWriter);
}
}
Then register your plugin in the servicestack apphost Configure()
Plugins.Add(new MySoapFeature());
Then create your Dto classes for the request and response. Have "Response" added to the response dto class name. Do NOT put a Route attribute on the request Dto, as it gets routed by the Soap method name in the Xml.
[DataContract(Namespace = "http://mynamespace/schemas/blah/1.0")]
public class MySoapMethod
{}
DataContract(Namespace = "http://mynamespace/schemas/blah/1.0")]
public class MySoapMethodResponse
{
[DataMember]
public string SomeProperty { get; set; }
}
Then have a Service to implement the Soap Dto's
public class SOAPService : Service
{
public MySoapMethodResponse Post(MySoapMethod request)
{
var response = new MySoapMethodResponse();
response.SomeProperty = "blah";
return response;
}
}
Trying a setup with ServiceStack 3.9.49 and CORS.
A simple Echo Service which returns the POSTed data back++. The code:
[Route("/echo")]
public class EchoRequest
{
public string Name { get; set; }
public int? Age { get; set; }
}
public class RequestResponse
{
public string Name { get; set; }
public int? Age { get; set; }
public string RemoteIp { get; set; }
public string HttpMethod { get; set; }
}
public class EchoService : Service
{
public RequestResponse Any(EchoRequest request)
{
var response = new RequestResponse
{
Age = request.Age,
Name = request.Name,
HttpMethod = base.Request.HttpMethod,
RemoteIp = base.Request.RemoteIp
};
return response;
}
}
The AppHost Configure code:
public override void Configure(Container container)
{
ServiceStack.Text.JsConfig.EmitCamelCaseNames = true;
SetConfig(new EndpointHostConfig
{
DefaultContentType = ContentType.Json,
GlobalResponseHeaders = new Dictionary<string, string>(),
DebugMode = true
});
Plugins.Add(new CorsFeature());
PreRequestFilters.Add((httpRequest, httpResponse) => {
//Handles Request and closes Responses after emitting global HTTP Headers
if (httpRequest.HttpMethod == "OPTIONS")
httpResponse.EndServiceStackRequest();
});
RequestFilters.Add((httpRequest, httpResponse, dto) =>
{
httpResponse.AddHeader("Cache-Control", "no-cache");
});
}
When sending a POST (with the json object in the body) with Content-Type: application/json, everything works great.
But when sending the same content and setting the Content-Type to text/plain, the correct method gets invoked, but the data in the EchoRequest is null.
Is this the correct behaviour? Must the Content-Type be set to application/json if a json object is sent as a POST?
Is yes, is it possible override this somehow e.g. in the url? From my understanding using ?format=json in the url, only affects the returned data...
Final question, is it possible to modify the Content-Type header of the request before being deserialized to the method, somewhere, something like this:
if (httpRequest.ContentType == "text/plain")
httpRequest.Headers["Content-Type"] = ContentType.Json;
Deserializing into an empty object is the correct behavior for ServiceStack's serializer. It tends to be very forgiving. It creates an empty deserialized object and proceeds to hydrate it with anything it parses out of the input, meaning if you give it junk data, you will get back an empty object.
You can make the serializer less forgiving by specifying the following option in your AppHost config:
ServiceStack.Text.JsConfig.ThrowOnDeserializationError = true;
I am not aware of any way to modify the URL to indicate to ServiceStack that the request is in JSON format. Furthermore, it doesn't appear there is any way inside ServiceStack to modify the content type before deserialization. Even specifying a PreRequestFilter to modify the header before will not work as the request's ContentType property has been set and is readonly.
PreRequestFilters.Add((req, res) => req.Headers["Content-Type"] = "application/json");
I can call my service using "/api/X" path via Javascript. (POST verb)
I can call same service without request object using client.Get(serviceUrl) //client is JsonServiceClient
But client.Send(X) does not work. I'm getting weird 404 NotFound response?
Am I missing something? And how can I debug problem?
The cost is 5 hours till now!
CheckList
X class have two string property (No enum or customType)
X and XResponse has DataContract and DataMember attributes (Trial)
Code:
In AppHost.cs
base.SetConfig(new EndpointHostConfig
{
GlobalResponseHeaders =
{
{ "Access-Control-Allow-Origin", "*" },
{ "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS" },
},
AppendUtf8CharsetOnContentTypes = new HashSet<string> { ContentType.Html },
DebugMode = true, //Show StackTraces in service responses during development
LogFactory = new ElmahLogFactory(new Log4NetFactory())
});
//Set JSON web services to return idiomatic JSON camelCase properties
ServiceStack.Text.JsConfig.EmitCamelCaseNames = true;
//Configure User Defined REST Paths
Routes.Add<JobRequest>("/job");
In Model.cs
[DataContract]
public class JobRequest : IReturn<JobRequestResponse>
{
[DataMember]
public string JobRequestEnum { get; set; }
[DataMember]
public string JobData { get; set; }
}
[DataContract]
public class JobRequestResponse : IHasResponseStatus
{
[DataMember]
public string Message { get; set; }
[DataMember]
public ResponseStatus ResponseStatus { get; set; }
}
In JobService.cs
public class JobService : ServiceStack.ServiceInterface.Service
{
public JobRepository Repository { get; set; }
public object Any(JobRequest request)
{
return new JobRequestResponse() { Message = "ok" };
}
}
In Javascript.js // IT WORKS
$.ajax({
type: 'POST',
url: "/api/job",
data: '{ "jobRequestEnum": "test", "jobData": "test" }',
dataType: "json",
contentType: "application/json",
success: function (res) {
debugger;
}
});
In CallJob.js // IT DOES NOT WORK
string serviceUrl = ConfigurationManager.AppSettings["serviceUrl"];
using (JsonServiceClient client = new JsonServiceClient(serviceUrl))
{
var request = new JobRequest() { JobData = "test", JobRequestEnum = "test" };
var response = client.Send<JobRequestResponse>(request);
}
If you add the [Route] on your Request DTO like:
[Route("/job")]
[DataContract]
public class JobRequest : IReturn<JobRequestResponse>
{
[DataMember]
public string JobRequestEnum { get; set; }
[DataMember]
public string JobData { get; set; }
}
The new IReturn C# Client APIs
Then the C# Client will be able to use the custom /job route on the client, e.g:
var client = new JsonServiceClient(serviceUrl);
var request = new JobRequest { JobData = "test", JobRequestEnum = "test" };
JobRequestResponse response = client.Post(request);
Whenever you don't have the Route defined on the Request DTO or specify the Response type, e.g:
var response = client.Send<JobRequestResponse>(request);
You're not using the IReturn<T> C# client API so it will send a POST to the pre-defined route which for this would be:
POST /json/syncreply/JobsRequest
{ ... }
Not /jobs as you're assuming (i.e. the C# client has no way to know of the route info, so it falls back to using the pre-defined routes).
Manually specifying to use the /customroute
Otherwise if you want to keep your Route definitions in AppHost you can force it to use the pre-defined url by supplying it in the C# call, e.g:
client.Post<JobRequestResponse>("/job", request);
No need for 'Response' suffix
Also in the new API (and when you use the IReturn<T> marker) you no longer need the Request DTO + 'Response' naming convention and are free to choose any name you need, so it doesn't have to be JobRequestResponse which sounds a little awkward :)