After last time introduction to GeoSIK and CSW, now is the time to implement our first CSW server. We will setup the basic infrastructure for our application and implement a basic service. A more complete (and useful) implementation will be shown in a future post.
For this tutorial you will need Visual Studio 2010 (Express Edition should be enough), NuGet Package Manager 1.6.1 and GeoSIK 0.9 Preview 1. This latest package is just a zip containing the GeoSIK binaries in a preview version that seems usable in a tutorial like this one. Proper releases will be delivered as NuGet packages.
The application
What we need first is an ASP .NET Application that will host our CSW service:
Then we will add the needed references. Add the following NuGet packages: LinqToXsd 2.0.2, Common.Logging 2.0.0 and Json.NET 4.0.8. Then add the following assemblies from the GeoSIK Preview package: GeoSik.dll, GeoSik.Services.dll, Irony.dll, Irony.Interpreter.dll, ProjNet.dll (you should recognize most of the dependencies I introduced in the previous post).
The service (I)
Then we need a WCF Service named Csw.svc:
Change the definition of the ICsw interface to the following:
[ServiceContract]Easy, right? Now implement the associated Csw class. You should have 5 methods: make them throw NotImplementedExceptions for now, we’ll implement them later.
public interface ICsw:
GeoSik.Ogc.WebCatalog.Csw.V202.IDiscovery
{
}
public class Csw:Now configure the web service by editing the Web.config file in a usual way. We will provide the CSW service in a POST+XML fashion (cf. specification §10.3.1). The GET+KVP encoding will be provided later. For this service to be fully CSW compliant, we will need to specify a custom behavior so that exceptions will be returned in XML too (cf. specification §10.3.5). This behavior is defined in the GeoSik.Services assembly, so the configuration reads as follows:
ICsw
{
public DescribeRecordResponse DescribeRecord(DescribeRecord request)
{
throw new NotImplementedException();
}
public Capabilities GetCapabilities(GetCapabilities request)
{
throw new NotImplementedException();
}
public GetDomainResponse GetDomain(GetDomain request)
{
throw new NotImplementedException();
}
public GetRecordByIdResponse GetRecordById(GetRecordById request)
{
throw new NotImplementedException();
}
public IGetRecordsResponse GetRecords(GetRecords request)
{
throw new NotImplementedException();
}
}
<system.serviceModel>
<services>
<service name="Tutorial.Csw" behaviorConfiguration="ows100PoxBehavior">
<endpoint address="xml" contract="Tutorial.ICsw" binding="webHttpBinding" behaviorConfiguration="poxBehavior" />
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior name="poxBehavior">
<webHttp />
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="ows100PoxBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="True" />
<ows100PoxFaultBehavior />
</behavior>
<behavior name="">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="ows100PoxFaultBehavior" type="GeoSik.Ogc.Ows.V100.PoxFaultBehaviorExtensionElement, GeoSik.Services" />
</behaviorExtensions>
</extensions>
</system.serviceModel>
Nothing fancy here if you are used to WCF services configurations: our service endpoint is defined.
The data source
Then we need to define a data source for our CSW server: our metadata store. This will usually be a database. In the context of this post though, we will define our metadata statically in memory. The link to a real database will be the subject of a following post.What we need first is define a class that contains our metadata (a record, in cswspeak). To keep things simple, we will only support the basic, Dublin Core based, information like an identifier, a title… What we need to take into account, though, is that a CSW client can query our metadata in 2 ways:
- with an alias (a core queryable, cf. specification §6.3.2). These will be handled thanks to the GeoSik.Ogc.WebCatalog.Csw.V202.CoreQueryableAttribute that can be applied to any property.
- with an XPath expression (cf. specification §10.6.4.9). To that effect, our record class has to be serializable in XML with the XmlSerializer. Do not worry: it will never be serialized this way. But if it were, the GeoSik XPath resolver would be able to find the nodes according to the resulting XML. Thus we will use the usual XML serialization attributes to customize the serialization.
[XmlRoot("Record", Namespace=Namespaces.OgcWebCatalogCswV202, IsNullable=false)]Notice how the virtual AnyText element is a simple combination of the text elements of our record.
public class Record:
IRecord
{
[XmlElement("identifier", Namespace=Namespaces.DublinCoreElementsV11, DataType="string", Order=0, IsNullable=false)]
[CoreQueryable(CoreQueryableNames.Identifier)]
public string Identifier { get; set; }
[XmlElement("title", Namespace=Namespaces.DublinCoreElementsV11, DataType="string", Order=1, IsNullable=false)]
[CoreQueryable(CoreQueryableNames.Title)]
public string Title { get; set; }
[XmlElement("subject", Namespace=Namespaces.DublinCoreElementsV11, DataType="string", Order=2, IsNullable=false)]
[CoreQueryable(CoreQueryableNames.Subject)]
public string Subject { get; set; }
[XmlElement("BoundingBox", Namespace=Namespaces.OgcOws, Order=3, IsNullable=false)]
[CoreQueryable(CoreQueryableNames.BoundingBox)]
public ISimpleGeometry Coverage { get; set; }
[XmlElement("AnyText", Namespace=Namespaces.OgcWebCatalogCswV202, DataType="string", Order=4, IsNullable=false)]
[CoreQueryable(CoreQueryableNames.AnyText)]
public string AnyText
{
get
{
return string.Join(" ", Identifier, Title, Subject);
}
}
public IRecordConverter GetConverter(XmlNamespaceManager namespaceManager)
{
return new RecordConverter(namespaceManager);
}
}
Our data source for this tutorial will simply be a manually created list of Record instances. But, you may ask, how do I initialize the Coverage property, which is a ISimpleGeometry? First of all: congratulations if you noticed that! The handling of geometries in GeoSik is way beyond this tutorial, so for now let me just show you an easy way to create geometries from their WKT definitions:
public static ISimpleGeometry CreateGeometry(string wkt)We will come back to this in another post. And I will let you initialize your Record instances as an exercise.
{
var builder=new GeoSik.Ogc.Gml.V311.GmlGeometryBuilder();
builder.Parse(wkt, ProjNet.CoordinateSystems.GeographicCoordinateSystem.WGS84);
return builder.ConstructedGeometry;
}
The service (II)
Let’s do this thing! What we need now is to plug our previous data source into the GeoSik CSW base implementation. For this we need to implement the abstract GeoSik.Ogc.WebCatalog.Csw.V202.Discovery class (the name of this class is a reference to the class defined in the specification §7.2.4). As you will see, there are only 2 methods to implement, and they are not overly difficult:public class Discovery:Obviously, you would have to properly initialize a list of records: this data source is pretty empty…
GeoSik.Ogc.WebCatalog.Csw.V202.Discovery
{
protected override IQueryable GetRecordsSource(Uri outputSchema)
{
return _RecordList.AsQueryable();
}
public override string ProviderName
{
get { return "CSW Tutorial"; }
}
private static List<Record> _RecordList=new List<Record>();
}
Now we can implement the Csw service class that we partially implemented sooner. I will show you all of it now:
public class Csw:
ICsw
{
public DescribeRecordResponse DescribeRecord(DescribeRecord request)
{
return _Implementation.DescribeRecord(request);
}
public Capabilities GetCapabilities(GetCapabilities request)
{
return _Implementation.GetCapabilities(request);
}
public GetDomainResponse GetDomain(GetDomain request)
{
return _Implementation.GetDomain(request);
}
public GetRecordByIdResponse GetRecordById(GetRecordById request)
{
return _Implementation.GetRecordById(request);
}
public IGetRecordsResponse GetRecords(GetRecords request)
{
return _Implementation.GetRecords(request);
}
private Discovery _Implementation=new Discovery();
}
The server
That’s it: our server is complete. Granted, there are a few drawbacks:- only the POST+XML protocol is supported, which makes it quite hard to test unless you have a tool like Fiddler (and you know how to use it).
- if you manage to launch a GetCapabilities request (the address of the service is something like ~/Csw.svc/xml/GetCapabilities), you will notice that no operation has been defined.
This could be fixed quite easily, but it goes beyond the scope of this post. And this will give us some work to do in the next tutorial, besides the connection to a database.
Those of you readers who are not very accustomed to the intricacies of the CSW specifications (though I think you should be if you want to mess with a CSW server) may not have been very impressed by all the above, but I know I have been surprised by the few lines of code this tutorial comprises. And even though there is still a lot of work to do, this makes me think that the project heads on a good direction. But let me know what you think.
For those interested, the source code of this tutorial can be