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 :)
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 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 am trying to implement validation feature in ServiceStack to validate my RequestDTO's before calling db operations.
When i try to validate request dto like
ValidationResult result = this.AddBookingLimitValidator.Validate(request);
the code automatically throws a validation error automatically.
I can not even debug service what is happening behind the scenes ? Can i change that behaviour or am i doing something wrong here.
Thanks.
My Request DTO :
[Route("/bookinglimit", "POST")]
[Authenticate]
public class AddBookingLimit : IReturn<AddBookingLimitResponse>
{
public int ShiftId { get; set; }
public DateTime Date { get; set; }
public int Limit { get; set; }
}
My Response DTO :
public class AddBookingLimitResponse
{
public int Id { get; set; }
public ResponseStatus ResponseStatus { get; set; }
}
Validation class :
public class AddBookingLimitValidator : AbstractValidator<AddBookingLimit>
{
public AddBookingLimitValidator()
{
RuleFor(r => r.Limit).GreaterThan(0).WithMessage("Limit 0 dan büyük olmalıdır");
}
}
Service Implementation :
public AddBookingLimitResponse Post(AddBookingLimit request)
{
ValidationResult result = this.AddBookingLimitValidator.Validate(request);
Shift shift = new ShiftRepository().Get(request.ShiftId);
BookingLimit bookingLimit = new BookingLimit
{
RestaurantId = base.UserSession.RestaurantId,
ShiftId = request.ShiftId,
StartDate = request.Date.AddHours(shift.StartHour.Hour).AddMinutes(shift.StartHour.Minute),
EndDate = request.Date.AddHours(shift.EndHour.Hour).AddMinutes(shift.EndHour.Minute),
Limit = request.Limit,
CreateDate = DateTime.Now,
CreatedBy = base.UserSession.UserId,
Status = (byte)Status.Active
};
return new AddBookingLimitResponse
{
Id = new BookingLimitRepository().Add(bookingLimit)
};
}
AppHost code :
container.RegisterValidators(typeof(AddBookingLimitValidator).Assembly);
Plugins.Add(new ValidationFeature());
And i consume the service in c# code:
try
{
AddBookingLimitResponse response = ClientHelper.JsonClient.Post(new AddBookingLimit
{
Date = DateTime.Parse(DailyBookingLimitDateTextBox.Text),
Limit = Convert.ToInt32(DailyBookingLimitTextBox.Text),
ShiftId = Convert.ToInt32(DailyDayTypeSelection.SelectedValue)
});
WebManager.ShowMessage(UserMessages.SaveSuccessful.FormatString(Fields.BookingLimit));
}
catch (WebServiceException ex)
{
WebManager.ShowMessage(ex.ResponseStatus.Message);
}
Right, ServiceStack validates the request DTO before the service gets called if the ValidationFeature is enabled.
To manually invoke the validator in the service, you have to remove this line from your AppHost first:
Plugins.Add(new ValidationFeature());
Please make sure that the validator property in your service has the type IValidator<>, otherwise it won't be injected by the IoC container if you register your validators with container.RegisterValidators(typeof(AddBookingLimitValidator).Assembly).
public class TestService : Service
{
public IValidator<Request> Validator { get; set; }
public RequestResponse Post(Request request)
{
Validator.Validate(request);
...
}
}
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");
});