Is there are a way to programmatically generate the /$metadata response returned from an ASP.Net Web Api OData controller route in a way that can be serialized to XML?
The reason I want to do this is that I'm using breeze to access the Web API using the OData adapter and would like to pre-load the Breeze MetadataStore with the metadata, like in this http://breeze.github.io/doc-js/metadata-load-from-script.htmlexample.
But this example does not seem to work with the OData adapter as it uses different metadata.
If I understand your question, you are trying to simulate GET /$metadata on the server so you can store the results in a file. In ASP.NET OData, $metadata is represented by an object that implements IEdmModel (e.g., the result of calling ODataModelBuilder.GetEdmModel). The problem then becomes how to serialize that model to XML.
The following helper will write service metadata to the given stream. For the model and config parameters, you should pass the same objects you used for your service configuration.
public class MetadataHelper
{
public static Task WriteMetadataAsync(Stream stream, IEdmModel model, HttpConfiguration config, string odataRouteName)
{
var request = new HttpRequestMessage(HttpMethod.Get, "/$metadata");
request.ODataProperties().Model = model;
request.ODataProperties().RouteName = odataRouteName;
request.SetConfiguration(config);
var payloadKinds = new List<ODataPayloadKind> { ODataPayloadKind.MetadataDocument };
var xmlMediaType = new MediaTypeHeaderValue("application/xml");
var formatter = new ODataMediaTypeFormatter(payloadKinds).GetPerRequestFormatterInstance(model.GetType(), request, xmlMediaType);
var content = new StringContent(String.Empty);
content.Headers.ContentType = xmlMediaType;
return formatter.WriteToStreamAsync(model.GetType(), model, stream, content, null);
}
}
True, the OData metadata is handled correctly by Breeze only when reading an OData response; the MetadataStore doesn't import/export it directly.
I think the easiest way to handle this is to create a separate bit of client-side code that will
Create an EntityManager that will
Fetch the metadata from the OData server
Export the metadata from the MetadataStore
Log the metadata so you can capture it and store it in a file
Not elegant, but it gets the job done.
Some future version of breeze.server.net will do the OData-to-Breeze metadata conversion on the server, so we won't have this problem.
Related
i'm evaluating Breeze.Js for a large enterprise, data oriented, Angular 5 application in order to take advantage of the following features that are missing in the vanilla Angular framework:
client side data store
client side model state tracking
client side model validation rules
bulk data persistence (SaveChanges() method to persist all entities).
For test purposes i've written the following simple BreezeController in my ASP.NET WebApi + EntityFramework server side:
[EnableCors(origins: "*", headers: "*", methods: "*")]
[BreezeController]
public class PeopleController : ApiController
{
private AdventureWorksDbContext db = new AdventureWorksDbContext();
#region "Breeze"
readonly EFContextProvider<AdventureWorksDbContext> _contextProvider =
new EFContextProvider<AdventureWorksDbContext>();
// ~/breeze/todos/Metadata
[HttpGet]
public string Metadata()
{
return System.Text.Encoding.UTF8.GetString(AdventureWorks.WebApi.Properties.Resources.WebApiMetadata);
}
// ~/breeze/todos/Todos
// ~/breeze/todos/Todos?$filter=IsArchived eq false&$orderby=CreatedAt
[HttpGet]
public IQueryable<PersonDTO> GetPeople()
{
return db.People.ProjectTo<PersonDTO>();
}
// ~/breeze/todos/SaveChanges
[HttpPost]
public SaveResult SaveChanges(Newtonsoft.Json.Linq.JObject saveBundle)
{
return _contextProvider.SaveChanges(saveBundle);
}
#endregion
}
As you can see in my example (it uses AdventureWorks DB) i've done the following modifications:
1) "GetPeople()" endpoint returns a queryable of DTO ("ProjectTo" extension is provided by Automapper). I need to do this in order to shape the model in a usable way for the client, avoid recursions, deep dive in the schema, big fields serialization and so on.
2) "Metadata()" endpoint returns a string resource that represents metadata of the DTO class. I builded it using "PocoMetadata" tool of the "Breeze Tooling Suite" (https://github.com/Breeze/breeze.tooling). This is needed because i can't return the _contextProvider.Metadata() result as long as i'm using DTO's and not EF POCO class.
Now, if in my Angular 5 client i issue an ODATA query like the following i can see that executeQuery() method actually works:
export class BreezeDataStoreComponent implements OnInit {
private _em: EntityManager;
constructor() {
this._em = new EntityManager({
serviceName: 'http://localhost:31328/breeze/People'
});
}
ngOnInit() {
const query = EntityQuery.from('GetPeople')
.where('FirstName', FilterQueryOp.StartsWith, 'F')
.orderBy('LastName', true);
this._em.executeQuery(query).then(res => {
// Here i can get all People instances.
// Now i try to get the first, edit the name and saveChanges.
(res.results[0] as any).FirstName = 'Franklino';
this._em.saveChanges().then(saveResult => {
const test = saveResult.entities;
});
});
}
}
Unfortunately problems comes with SaveChanges().
When the Angular client calls that method, in my server side i get the following error:
System.InvalidOperationException: Sequence contains no matching
element
I think it's due to the fact that i'm calling SaveChanges() over an EF context provider passing a JObject bundle referred to DTO instead of POCO class.
So my question is:
Is it possible to use BreezeJs query and bulk persistence (SaveChanges() method) using DTO's? It's a pretty common need in big data-centric enterprise applications since i think it's a bad practice exposing EF POCOs on WebApi.
should i rely instead over a classic WebApi that respond to the POST\PUT\DELETE HTTP verbs? In that case, how to configure Breeze client in order to contact those endpoints instead of "SaveChanges" when persisting data?
If Breeze is not suitable for this needs are there other technolgies that provides the 4 abovementioned points?
Thank you very much.
To make SaveChanges work with your DTOs, you would need to either
Write your own method to unpack the JObject saveBundle, or
Use the BeforeSaveChanges method to modify the dictionary of DTOs and replace them with entities that EF understands.
Number 2 seems like the better choice. If you do not have a 1:1 match between entities and DTOs, some logic would be required when doing the mapping.
According to this tutorial: http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/using-select-expand-and-value
"Web API 2 adds support for the $expand, $select, and $value options in OData. These options allow a client to control the representation that it gets back from the server"
My question it, how can I disable the representation manipulation done at the client side. In other words, my server makes sure that filtering/selecting etc. are done properly, and thus I do not want the client side to do it again. It is more of an overhead.
I think you misunderstand the purpose of query options like $expand, $select, etc. They do not cause data to be manipulated on the client. Rather, they are instructions to the service. In the Web API OData implementation, query options are typically handled by the EnableQuery attribute or the Queryable attribute. If you don't use these attributes, then you are responsible for writing the code that handles query options. Or you are free to not support them.
In your controller action, like get method, add attribute [EnableQuery] (this is for OData v4)
IN your client, send out request like ~/EntitySet?$filter=... & $select = ...
Then the response will only contain the filtered and select content.
Refer to https://github.com/OData/ODataSamples/tree/master/WebApi/v4/ODataQueryableSample to see the example.
You can create a custom attribute which would inherit from EnableQueryAttribute and then override the ValidateQuery method to limit the allowed query options as well as allowed functions and page size.
using System.Net.Http;
using System.Web.OData;
using System.Web.OData.Query;
public class SecureApiQueryAttribute : EnableQueryAttribute
{
public override void ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions)
{
base.AllowedQueryOptions = AllowedQueryOptions.None;
base.PageSize = 30;
base.AllowedFunctions = AllowedFunctions.AllFunctions;
base.ValidateQuery(request, queryOptions);
}
}
Then you can use this custom attribute like this
[SecureApiQuery]
public IHttpActionResult Get([FromODataUri] int? key = null)
{
}
Given this code in a C# REST api. A request comes in and a method is invoked as below
using (MethodInformation _methodInformation = new MethodInformation())
{
_methodInformation.ObjectType = _functionElement.Type;
_methodInformation.Method = _functionElement.Method;
_methodInformation.RequestStream = ConvertRequestContent();
_methodInformation.RequestParameters = ConvertRequestParameters();
_object = _methodInformation.InstanceMethodInvoker(null);
}
All data from the request is packed up in to _methodInformation.RequestStream.
We now lose the default model binding functionality that the MVC framework provides.
Is this a legitimate way to go about this, bearing in mind we cannot use model binding and have to reconstruct the strongly typed objects from the RequestStream.
What advantages can we get from implementing the API this way?
I am trying to adapt this example to create a simple OData client. Before that, I added a service reference in Visual Studio to "http://services.odata.org/Northwind/Northwind.svc/".
By this step I got many classes like "Alphabetical_list_of_product". But how do I get the alphabetical list of products, for example?
Specifically, in the example the author just starts with:
OdataClient.NorthwindOdataService.NorthwindEntities dc =
new OdataClient.NorthwindOdataService.NorthwindEntities(
new Uri("http://services.odata.org/Northwind/Northwind.svc/"));
But where did he get the OdataClient.NorthwindOdataService.NorthwindEntities class from?
I am new to web services and OData, so apologies if the question is vague.
Here is an example of how the service reference can be used after it has been added to the project:
// Create a service context object
// "NorthwindEntities" is the name of the class in the generated service reference that derives DataServiceContext
// The URI in should be the same URI you used to add the service reference
var context = new NorthwindEntities(new Uri("http://services.odata.org/Northwind/Northwind.svc/"));
// As Alphabetical_list_of_products is an entity set, it can be directly called from the context
// Call Execute() finally to send the request to the OData service and materialize the response got to "products"
var products = context.Alphabetical_list_of_products.Execute();
// Iterate through all the products and print "ProductName", which is the name of a property on "Alphabetical_list_of_product" entity
foreach (var product in products)
{
Console.WriteLine(product.ProductName);
}
As you are new to OData, it is recommended that you start from OData V4. Add Service Reference supports client side proxy generation of OData service up to OData V3. The OData V4 protocol on OASIS Comittee and the blog of the OData team of Microsoft can be referred to for details.
If you want a client for consuming OData service, a good choice should be OData code generator. You can start by reading the tutorial http://blogs.msdn.com/b/odatateam/archive/2014/03/12/how-to-use-odata-client-code-generator-to-generate-client-side-proxy-class.aspx .
I am currently having 2 issues in service stack. I am currently trying to build a service to imitate an existing server software. This requires a few things that i am having issues with.
This is using a self hosted servicestack instance and latest version
I need to have service on "/" that takes no paramters.
All my services need to return results using a customer XML serializer not the data contact one no matter what is in the accept header. (currently return html representation of DTO)
For issue 1 i have been using [FallbackRoute("/")] which is working but then no matter what i do i can't get my custom serializer to be used.
For issue 2 i made a custom serializer using the dotnet xml serializer that will generate the output i need and registered it as a ContentTypeFilters. I then manually set the response type header but this did not trigger my serializer. This is really starting to drive me nuts as i need to implement about 20 services and i can't even get the simple root service working let alone the rest of them.
Basically my XML is in a format the DataContract serializer can't handle and the url's and content must be an exact match for the existing system.
It looks like both issue 1 and issue 2 are really the same issue; Your custom serialiser isn't getting called. This is either an issue with registering your serialiser, returning the content type or both. Below shows how you should set it up. Using ServiceStack v4:
Register your custom serialiser:
In your AppHost Configure method you need to register your custom XML serialiser:
StreamSerializerDelegate serialize = (request, response, stream) => {
// Replace with appropriate call to your serializer and write the output to stream
var myCustomSerializer = new MyCustomSerializer(response);
stream.write(myCustomerSerializer.getResult());
};
StreamDeserializerDelegate deserialize = (type, fromStream) => {
// Implement if you expect to receive responses using your type
throw new NotImplementedException();
};
// Register these methods to run if content type 'application/xml' is sent/received
ContentTypes.Register("application/xml", serialize, deserialize);
Set the return content type:
In your service you need to set the return content type, so the serialiser knows to run. You can do this either by adding an attribute on each method than needs to use this type, or if all your methods return this type you can configure it as the default.
Per method basis:
You can use the AddHeader attribute with the ContentType parameter. i.e:
public class TestService : Service
{
[AddHeader(ContentType = "application/xml")]
public TestResponse Get(RootRequest request)
{
return new TestResponse { Message = "Hello from root" };
}
}
All methods return this type:
You can set the default content type in the AppHost Configure method. i.e:
public override void Configure(Funq.Container container)
{
SetConfig(new HostConfig {
DebugMode = true,
DefaultContentType = "application/xml"
});
}
Fully working demo app
The demo is a self hosted console app, that takes a request to the root / or to /Test and returns a custom serialised response.
Hope this helps.