I have an MVC solution that contains a ServiceStack API alongside an MVC UI that makes calls to the API services. Some of those services are AutoQuery GET endpoints, and I'm running into a problem where the ServiceStack service picks up posted form values or unrelated querystring values and throws argument errors when I call the services.
I've tried a number of ways of calling the services:
using (var fooSvc = new HostContext.ResolveService<FooService>(HttpContext))
{
var foos = (QueryResponse<Foo>)fooSvc.Get(new Foos());
// Do stuff here
}
No dice. Posted form values screw it up and I get a System.FormatException saying Input string was not in a correct format on this line in my service:
var q = AutoQuery.CreateQuery(request, Request.GetRequestParams());
So I tried:
var foos = (QueryResponse<Foo>)HostContext.ServiceController.Execute(new Foos());
In this case, I get a System.NotImplementedException saying it couldn't find a Post(Foos) or Any(Foos) (the call in question is a GET).
I'm sure I'm missing something simply. Mythz, got another rescue for me?
EDIT: I hand-typed that code...the initial block had AutoQuery when I meant QueryResponse...
EDIT 2: Here is the general structure of my AutoQuery services. These are all GET on the service because those endpoints also need to support POST to create a resource. For example, I might have the URI at http:/service.com/api/users and want to be able to GET with AutoQuery or POST to create a new user.
[Authenticate]
public class UsersService : Service
{
public IAutoQuery AutoQuery { get; set; }
public object Get(Users request)
{
var q = AutoQuery.CreateQuery(request, Request.GetRequestParams());
// Here I add some other conditions to the query object to filter results based on the user's role, etc.
return AutoQuery.Execute(request, q);
}
[RequiredRole("Admin")]
public object Post(CreateUser request)
{
var user = request.ConvertTo<User>();
Db.Save(user);
return user;
}
}
When you call a Service you're just calling a method directly on the target instance. What does the method returns? It's unlikely it's AutoQuery<Foo> since it's not a valid type in ServiceStack:
var foos = (AutoQuery<Foo>)fooSvc.Get(new Foos());
If you're getting a FormatException check the QueryString param that's causing it. Something in the Request is incompatible with the AutoQuery field.
Did you create the method yourself because AutoQuery generates services using the Any method so that it can be called by any verb. If it's a Custom Service change it to Any.
Otherwise you can Execute the method with the current HTTP Request with:
var foos = (QueryResponse<Foo>)HostContext.ServiceController
.Execute(new Foos(), HttpContext.ToRequest());
You can also override what Verb that gets executed with:
var req = HttpContext.ToRequest();
req.Items[Keywords.InvokeVerb] = "GET";
var foos = (QueryResponse<Foo>)HostContext.ServiceController
.Execute(new Foos(), req);
Custom AutoQuery Implementation
If you have conflicted fields trying to forward the request I would just change them to use fields that don't conflict with AutoQuery request. But if you still want to continue using the conflicted fields you can create a
Custom AutoQuery Implementation that removes it from the AutoQuery services instead, e.g:
public class UsersService : Service
{
public IAutoQuery AutoQuery { get; set; }
public object Any(Users request)
{
Dictionary<string, string> args = Request.GetRequestParams();
args.Remove("OrganizationId");// E.g remove conflicted fields
var q = AutoQuery.CreateQuery(request, args);
return AutoQuery.Execute(request, q);
}
}
Related
I have a service method:
public long InsertMessage(OutgoingInEntity input)
{
var request = new InsertOutgoingMessageRequest
{
Id = input.Id
... // fields
};
return Util.UsingWcfSync<IOutgoing, long>(client => client.InsertOutgoing(request));
}
I want to reuse this method in other context because I want 1 method which call this specific service, but the parameter OutgoingInEntity can change. When I call this method with other entities, the fields used in InsertOutgoingMessageRequest will be available and I will map like I did with the var request I cannot initiate InsertOutgoingMessageRequest in other context.
How can I say this input parameter is like generic and can be used for all kind of entities?
If you want to manage the object you receive you can just do this:
public long InsertMessage(Object input)
{
OutgoingInEntity yourObj = (OutgoingInEntity)input;
///.. your code ..///
}
Then you can do just the same for whatever you need.
I have a controller method which returns a list of resources like this:
[HttpGet, Route("events/{id:int}/rosters")]
public RosterExternalList List(int id, int page = 1, int pageSize = 50) {
return Repository.GetRosters(id).ToExternal(page, pageSize);
}
And the repository method is:
public IQueryable<EventRoster> GetRosters(int id) {
return EventDBContext.EventRosters.Where(x => x.eventID == id).OrderBy(x => x.EventRosterID).AsQueryable();
}
This is the same pattern for multiple list methods. The problem is that when no items are retrieved from the db, the api sends a 200 with an empty response body even if the id passed in is invalid.
What I want is if id = invalid, send appropriate response (404?). Otherwise, if the id is valid, and there are no records, send the 200 with empty body.
My question is - is this the right way to handle this? Can this be done via an Action Filter so it will be implemented across all the methods like this? How?
It is possible with an action filter like this:
public class EventsFilter : ActionFilterAttribute
{
public EventDBContext EventDBContext { get; set; }
public override void OnActionExecuting(HttpActionContext actionContext)
{
bool exists = false;
var routeData = actionContext.Request.GetRouteData();
object value;
if (routeData.Values.TryGetValue("id", out value))
{
int id;
if (int.TryParse(value, out id))
{
exists = EventDBContext.EventRosters.Where(x => x.eventID == id).Any();
}
}
if (exists == false)
{
var response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, "Event not found");
throw new HttpResponseException(response);
}
}
}
You would also need to configure a dependency resolver to set EventDBContext property.
Is it the right way?
It depends on your solution design:
If your business layer is integrated and depends on the Web API (as it appears from your example), then yes it is the way to go.
If Web API (service layer) is not the only one who uses your business layer, then you would want to avoid duplication of event checking in other places and move it to a more generic class than Web API action filter and call this class from your business layer instead of Web API to make sure that regardless of the caller, you would always check if event exists. In that class you would throw something like BusinessLogicNotFoundException with information about event. And on the Web API, you would need to create an ExceptionFilterAttribute that handles BusinessLogicNotFoundException and creates an appropriate 404 response.
Just a quick question, is this not supported in a Win 8.1 universal class library? Or if it is, can anyone help with what i'm doing wrong.
http://jbsapplication.azurewebsites.net/Modules?$filter=Name%20eq%20'JBS%20Electronic%20forms'&$expand=Menus
When I do this from the browser or Fiddler I receive the correct response.
My code in the client view model class is as follows (using the OData Client v2 code generated objects)
var application = new UriBuilder(ServiceBaseAddress);
var context = new Models.Application(application.Uri);
var modulesQuery = context.Modules.Expand(m=>m.Menus).Where(m => m.Name == ApplicationName);
var modules = await ((DataServiceQuery<Module>) modulesQuery).ExecuteAsync();
_currentModule = modules.FirstOrDefault();
The following exception is generated on the last line
A first chance exception of type 'Microsoft.OData.Core.ODataException' occurred in Microsoft.OData.Core.DLL
Additional information: When writing a JSON response, a user model must be specified and the entity set and entity type must be passed to the ODataMessageWriter.CreateODataEntryWriter method or the ODataFeedAndEntrySerializationInfo must be set on the ODataEntry or ODataFeed that is being writen.
If I remove the Expand portion of the request, everything is fine, but I need to then perform another round trip to get the menus.
A cut down reference for the Module class:
[Key("Id")]
public class Module: BindableBase
{
public string Name
{
get { return _name; }
set { SetProperty(ref _name, value); }
}
DataServiceCollection<Menu> _menus = new DataServiceCollection<Menu>(null,TrackingMode.AutoChangeTracking);
public DataServiceCollection<Menu> Menus
{
get { return _menus; }
set
{
_menus = value;
OnPropertyChanged("Menus");
}
}
}
I have encountered the problem you describe when I forgot to add the expanding entity into the ODataModelBuilder as an EntitySet. Try this in your ASP.NET OData Web API:
builder.EntitySet<Menus>("Menus");
Models with an ID property need to be explicitly expanded by clients, and expandable models need to be registered as entitysets with the builder for the auto-generated OData client to be able to call expand.
I am attempting to get ServiceStack to return a list of objects to a C# client, but I keep getting this exception:
"... System.Runtime.Serialization.SerializationException: Type definitions should start with a '{' ...."
The model I am trying to return:
public class ServiceCallModel
{
public ServiceCallModel()
{
call_uid = 0;
}
public ServiceCallModel(int callUid)
{
this.call_uid = callUid;
}
public int call_uid { get; set; }
public int store_uid { get; set; }
...... <many more properties> ......
public bool cap_expense { get; set; }
public bool is_new { get; set; }
// An array of properties to exclude from property building
public string[] excludedProperties = { "" };
}
The response:
public class ServiceCallResponse
{
public List<ServiceCallModel> Result { get; set; }
public ResponseStatus ResponseStatus { get; set; } //Where Exceptions get auto-serialized
}
And the service:
public class ServiceCallsService : Service
{
// An instance of model factory
ModelFactory MyModelFactory = new ModelFactory();
public object Any(ServiceCallModel request)
{
if (request.call_uid != 0)
{
return MyModelFactory.GetServiceCalls(request.call_uid);
} else {
return MyModelFactory.GetServiceCalls() ;
}
}
}
The client accesses the service with:
JsonServiceClient client = new ServiceStack.ServiceClient.Web.JsonServiceClient("http://172.16.0.15/");
client.SetCredentials("user", "1234");
client.AlwaysSendBasicAuthHeader = true;
ServiceCallResponse response = client.Get<ServiceCallResponse>("/sc");
The "model factory" class is a DB access class which returns a list. Everything seems to work just fine when I access the service through a web browser. The JSON returned from the service starts:
"[{"call_uid":70...."
And ends with:
"....false,"is_new":true}]"
My question is, what here might be causing serialization/deserialization to fail?
Solution
Thanks to the answer from mythz, I was able to figure out what I was doing wrong. My misunderstanding was in exactly how many DTO types there are and exactly what they do. In my mind I had them sort of merged together in some incorrect way. So now as I understand it:
Object to return (In my case, called "ServiceCallModel": The actual class you wish the client to have once ServiceStack has done its job. In my case, a ServiceCallModel is a key class in my program which many other classes consume and create.
Request DTO: This is what the client sends to the server and contains anything related to making a request. Variables, etc.
Response DTO: The response that the server sends back to the requesting client. This contains a single data object (ServiceCallModel), or in my case... a list of ServiceCallModel.
Further, exactly as Mythz said, I now understand the reason for adding "IReturn" to the request DTO is so the client will know precisely what the server will send back to it. In my case I am using the list of ServiceCallModel as the data source for a ListView in Android. So its nice to be able to tell a ListViewAdapter that "response.Result" is in fact already a useful list.
Thanks Mythz for your help.
This error:
Type definitions should start with a '{'
Happens when the shape of the JSON doesn't match what it's expecting, which for this example:
ServiceCallResponse response = client.Get<ServiceCallResponse>("/sc");
The client is expecting the Service to return a ServiceCallResponse, but it's not clear from the info provided that this is happening - though the error is suggesting it's not.
Add Type Safety
Although it doesn't change the behavior, if you specify types in your services you can assert that it returns the expected type, e.g Change object to ServiceCallResponse, e.g:
public ServiceCallResponse Any(ServiceCallModel request)
{
...
}
To save clients guessing what a service returns, you can just specify it on the Request DTO with:
public class ServiceCallModel : IReturn<ServiceCallResponse>
{
...
}
This lets your clients have a more succinct and typed API, e.g:
ServiceCallResponse response = client.Get(new ServiceCallModel());
instead of:
ServiceCallResponse response = client.Get<ServiceCallResponse>("/sc");
See the New API and C# Clients docs for more info.
I know that "join" is not supported on client side with WCF DS thats why i decided to add a method on server side to do the "join" and return the result as custom type object.
Service looks like this:
public class CWcfDataService : DataService<CEntities>
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
config.UseVerboseErrors = true;
config.RegisterKnownType(typeof(CASE_STAGE_HISTORY_EXTENDED));
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
[WebGet]
public IQueryable<CASE_STAGE_HISTORY_EXTENDED> GetCASE_STAGE_HISTORY_EXTENDEDByDocId(int docId)
{
CEntities context = new CEntities();
return (from c in context.CASE_STAGE_HISTORY
join a in context.USRs on c.CREATOR_ID equals a.USRID
select new CASE_STAGE_HISTORY_EXTENDED()
{
CASE_STAGE_ID = c.CASE_STAGE_HISTORY_ID,
CASE_STAGE_NAME = c.CASE_STAGE_NAME,
CREATE_DATE = c.CREATE_DATE,
CREATOR_ID = c.CREATOR_ID,
DOC_ID = c.DOC_ID,
LAST_VARIANT_DOCUMENT_ID = c.LAST_VARIANT_DOCUEMENT_ID,
CREATOR_FULLNAME = a.FULLNAME
});
}
}
And custom class is:
[DataServiceKey("CASE_STAGE_ID")]
public class CASE_STAGE_HISTORY_EXTENDED
{
public int CASE_STAGE_ID { get; set; }
public int DOC_ID { get; set; }
public string CASE_STAGE_NAME { get; set; }
public int? LAST_VARIANT_DOCUMENT_ID { get; set; }
public DateTime? CREATE_DATE { get; set; }
public int? CREATOR_ID { get; set; }
public string CREATOR_FULLNAME { get; set; }
}
When i try to update service reference in Visual Studio i constantly get error:
The server encountered an error
processing the request. The exception
message is 'Unable to load metadata
for return type
'System.Linq.IQueryable1[CWcf.Code.CASE_STAGE_HISTORY_EXTENDED]'
of method
'System.Linq.IQueryable1[CWcf.Code.CASE_STAGE_HISTORY_EXTENDED]
GetCASE_STAGE_HISTORY_EXTENDEDByDocId(Int32)'.'.
See server logs for more details.
If i remove the public IQueryable<CASE_STAGE_HISTORY_EXTENDED> GetCASE_STAGE_HISTORY_EXTENDEDByDocId(int docId) part - on updating service reference i get another error:
The server encountered an error
processing the request. The exception
message is 'Internal Server Error. The
type
'CourtWcf.Code.CASE_STAGE_HISTORY_EXTENDED'
is not a complex type or an entity
type.'.
Environment: Visual Studio 2010, .NET 4.
I assume the data service is based on an Entity Framework model (the CEntities class is an ObjectContext).
If that's the case the types are completely read from the EF model (CSDL) and the class definitions are more or less ignored. So you need to define the type returned by your service operation in your EF model. Also note that for the IQueryable to work, that type has to be recognized by EF as an entity type (which might require mapping to the database, or something, EF experts will know more).
Added foreign keys and implemented LoadProperty. See the article from which i took this solution: http://thedatafarm.com/blog/data-access/the-cost-of-eager-loading-in-entity-framework/
Still if i have NO relations in database(for example i have no foreign keys)-i have another solution: create stored procedure and map it with import in your DB model. After that create complex type that will work in client too(tho you will have to access it by URI, not with lambda extensions). See example here. Many thanks for other answerers in this thread, you have made me look deeper into the subject.
For starters, WCF does not support IQueryable. That's your problem right there.
In your case IEnumerable should work.
You should look at a service as something that has methods and returns "data". This data could be single values, object instances or collections of objects. The client should not have to think about call a service to do a join, but rather, "give me the data for such and such - given these parameters".
You method name imparts the correct "intent" GetCaseStageHistoryExtendedByDocId(int docId), and what you get back is a collection of CASE_STAGE_HISTORY_EXTENDED objects. That's it.
An IQueryable implies something completely different and that concept does not pertain to a service as such.
EDIT
Try converting your Queryable to a List by calling the ToList() method on it.
return (from c in context.CASE_STAGE_HISTORY
join a in context.USRs on c.CREATOR_ID equals a.USRID
select new CASE_STAGE_HISTORY_EXTENDED()
{
CASE_STAGE_ID = c.CASE_STAGE_HISTORY_ID,
CASE_STAGE_NAME = c.CASE_STAGE_NAME,
CREATE_DATE = c.CREATE_DATE,
CREATOR_ID = c.CREATOR_ID,
DOC_ID = c.DOC_ID,
LAST_VARIANT_DOCUMENT_ID = c.LAST_VARIANT_DOCUEMENT_ID,
CREATOR_FULLNAME = a.FULLNAME
}).ToList();
Note that you can only send serializable objects across to the client. So first make sure your objects are serializable.