NuGet package feeds provide metadata about packages, but after searching
for new packages to use or updates to existing ones, we also have to
be able to download the actual package.
The NuGet OData implementation provides links to download a package
by using the default stream
concept.
In WCF Data Services, an entity indicates that it has a stream using the
aptly named HasStreamAttribute
.
1
2
3
4
5
6
[DataServiceKey("Id", "Version")]
[HasStream]
public class DataServicePackage : IEquatable < DataServicePackage >
{
//snip
}
To build a URI for each instance, a custom implementation of
IDataServiceStreamProvider
is wired into our DataService
.
This is done differently in WebApi which does not include the
HasStreamAttribute
at all.
Over a year ago, I asked on twitter:
At the time, the answer was no. However, I’m happy to report that
recent versions of WebApi do support this feature.
First, we need to tell our EDM model builder that the entity has
a default stream:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void MapODataRoutes ( HttpConfiguration config )
{
var builder = new ODataConventionModelBuilder ();
var entity = builder . EntitySet < ODataPackage >( "Packages" );
entity . EntityType . HasKey ( pkg => pkg . Id );
entity . EntityType . HasKey ( pkg => pkg . Version );
var entityType = model
. FindDeclaredType ( typeof ( ODataPackage ). FullName );
model = builder . GetEdmModel ();
model . SetHasDefaultStream (
entityType as IEdmEntityType , hasStream : true );
// snip
}
Next we’ll need to implement a custom ODataSerializerProvider
and ODataEdmTypeSerializer
to provide a default stream URL for
entity instances:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public class ODataPackageDefaultStreamAwareSerializerProvider
: DefaultODataSerializerProvider
{
private readonly ODataEdmTypeSerializer entitySerializer ;
public ODataPackageDefaultStreamAwareSerializerProvider ()
{
this . entitySerializer =
new ODataPackageDefaultStreamAwareEntityTypeSerializer ( this );
}
public override ODataEdmTypeSerializer GetEdmTypeSerializer (
IEdmTypeReference edmType )
{
if ( edmType . IsEntity ())
{
return entitySerializer ;
}
return base . GetEdmTypeSerializer ( edmType );
}
}
public abstract class DefaultStreamAwareEntityTypeSerializer < T >
: ODataEntityTypeSerializer where T : class
{
protected DefaultStreamAwareEntityTypeSerializer (
ODataSerializerProvider serializerProvider )
: base ( serializerProvider )
{
}
public override ODataEntry CreateEntry (
SelectExpandNode selectExpandNode ,
EntityInstanceContext entityInstanceContext )
{
var entry = base . CreateEntry ( selectExpandNode ,
entityInstanceContext );
var instance = entityInstanceContext . EntityInstance as T ;
if ( instance != null )
{
var link = BuildLinkForStreamProperty (
instance , entityInstanceContext );
entry . MediaResource = new ODataStreamReferenceValue
{
ContentType = ContentType ,
ReadLink = new Uri ( link )
};
}
return entry ;
}
protected virtual string ContentType
{
get { return "application/octet-stream" ; }
}
protected abstract string BuildLinkForStreamProperty (
T entity ,
EntityInstanceContext entityInstanceContext );
}
public class ODataPackageDefaultStreamAwareEntityTypeSerializer : DefaultStreamAwareEntityTypeSerializer < ODataPackage >
{
public ODataPackageDefaultStreamAwareEntityTypeSerializer (
ODataSerializerProvider serializerProvider )
: base ( serializerProvider )
{
}
protected override string BuildLinkForStreamProperty (
ODataPackage package ,
EntityInstanceContext context )
{
var url = new UrlHelper ( context . Request );
var routeParams = new { package . Id , package . Version };
return url . Link ( RouteNames . Packages . Download , routeParams );
}
protected override string ContentType
{
get { return "application/zip" ; }
}
}
The most relevant part above is the override of CreateEntry
(line 33) which sets the MediaResource
property on the
serialized ODataEntry
.
Finally the custom serializer is wired into the configuration:
1
2
3
4
config . Formatters . InsertRange ( 0 ,
ODataMediaTypeFormatters . Create (
new ODataPackageDefaultStreamAwareSerializerProvider (),
new DefaultODataDeserializerProvider ()));
This whole mess of code nets us a modest customization in the Atom
output from WebApi OData:
1
2
3
4
5
6
7
<entry>
<id> http://example/api/odata/Packages(Id='Lucene.Net.Linq',Version='3.2.55')</id>
<content
type= "application/zip"
src= "http://example/api/packages/Lucene.Net.Linq/3.2.55/content" />
<!-- snip -->
</entry>
This enables the NuGet client to download packages.
Series Index
Introduction
Basic WebApi OData
Composite Keys
Default Streams
Comments