I have a streaming WCF OData service hosted in IIS that is accessing a mysql database to store images as blobs. To implement it I followed the instructions in
Data Services Streaming Provider Series: Implementing a Streaming Provider (Part 1). I also built a WPF client using the Microsoft.Data.Services.Client library that creates, reads, updates, and deletes images through the service according to
Data Services Streaming Provider Series-Part 2: Accessing a Media Resource Stream from the Client. Everything works fine until I secure the service using Basic Authentication with SSL and HTTPS.
When authorization is required, read, update, and delete still work, but the creation of new records fails.
For successful creation request over HTTP I see the following request pattern in my IIS logs:
2013-06-25 11:39:13 ::1 POST /PictureService/PictureData.svc/PhotoInfo - 80 - ::1 Microsoft+ADO.NET+Data+Services 201 0 0 59476
2013-06-25 11:39:13 ::1 MERGE /PictureService/PictureData.svc/PhotoInfo(75) - 80 - ::1 Microsoft+ADO.NET+Data+Services 204 0 0 11
For successful update requests over HTTPS this is what is logged:
2013-06-25 12:17:34 ::1 PUT /AddressBookService/AddressBookService.svc/PictureInfos(AddressBookEntryId=1,AddressBookId=1,PictureInfoId=6)/$value - 443 - ::1 Microsoft+ADO.NET+Data+Services 401 1 0 5
2013-06-25 12:17:38 ::1 PUT /AddressBookService/AddressBookService.svc/PictureInfos(AddressBookEntryId=1,AddressBookId=1,PictureInfoId=6)/$value - 443 Alice ::1 Microsoft+ADO.NET+Data+Services 204 0 0 4045
2013-06-25 12:17:38 ::1 MERGE /AddressBookService/AddressBookService.svc/PictureInfos(AddressBookEntryId=1,AddressBookId=1,PictureInfoId=6) - 443 - ::1 Microsoft+ADO.NET+Data+Services 401 1 0 3
2013-06-25 12:17:39 ::1 MERGE /AddressBookService/AddressBookService.svc/PictureInfos(AddressBookEntryId=1,AddressBookId=1,PictureInfoId=6) - 443 Alice ::1 Microsoft+ADO.NET+Data+Services 204 0 0 926
Each failed request without authorization header is followed by a successful one in which the authorization header for user "Alice" is included. There is one PUT and one MERGE request for each update operation as described in the above links.
Upon a creation request, though, only one POST is issued which fails due to the absence of an authorization header.
2013-06-25 12:19:35 ::1 POST /AddressBookService/AddressBookService.svc/PictureInfos - 443 - ::1 Microsoft+ADO.NET+Data+Services 401 1 0 4
It appears as if the Microsoft.Data.Services.Client library forgets to answer the SSL challenge with a follow-up request containing the required authorization header.
Stepping through the service code, I found that in the Dispose() method of my IDataServiceStreamProvider implementation, the _picture entity (created inside GetWriteStream()) is correctly initialized using HTTP so that I can retrieve the matching PictureData
record if it exists or create a new one if it does not. But when a create request is processed over HTTPS, _picture has all default nulls and 0s.
public Stream GetWriteStream(object entity, string etag, bool? checkETagForEquality, DataServiceOperationContext operationContext)
{
if (checkETagForEquality != null)
{
// This stream provider implementation does not support ETags associated with BLOBs.
// This means that we do not track concurrency for a media resource
// and last-in wins on updates.
throw new DataServiceException(400, "This service does not support ETags associated with BLOBs");
}
PictureInfo image = entity as PictureInfo;
if (image == null)
{
throw new DataServiceException(500, "Internal Server Error: the Media Link Entry could not be determined.");
}
// Set the file name from the Slug header; if we don't have a
// Slug header, just set a temporary name which is overwritten
// by the subsequent MERGE request from the client.
image.FileName = operationContext.RequestHeaders["Slug"] ?? "newFile";
// Set the content type, which cannot be null.
image.ContentType = operationContext.RequestHeaders["Content-Type"];
MemoryStream photoStream = new MemoryStream();
// Cache image and stream in order to create or update the PhotoData record
// while maintaining transactional integrity in the disposer.
// Note that it is not enough to remmeber only the PhotoId,
// since the PictureInfo entity needs to be deleted from the database if something goes wrong.
_picture = image;
_pictureStream = photoStream;
return photoStream;
}
public void Dispose()
{
if (_pictureStream != null)
{
try
{
using (AddressBookEntities another = new AddressBookEntities())
{
// Copy the image data into the corresponding PhotoData record
PictureData pictureData = (from pd in another.PictureDatas
where pd.PictureInfoId == _picture.PictureInfoId
select pd).SingleOrDefault();
if (pictureData == null) {
// Create a new PictureData record and connect it to the corresponding PictureInfo record
pictureData = new PictureData
{
PictureInfoId = _picture.PictureInfoId,
AddressBookEntryId = _picture.AddressBookEntryId,
AddressBookId = _picture.AddressBookId
};
another.PictureDatas.AddObject(pictureData);
}
pictureData.PictureStream = _pictureStream.GetBuffer();
another.SaveChanges();
_pictureStream = null;
}
}
catch
{
// A problem occurred when saving the entity to the database,
// hence delete the entity.
_context.DeleteObject(_picture);
throw new DataServiceException("An error occurred. The photo could not be saved.");
}
}
I don't even know where to start to make this work. Is the Client library at fault or did I misunderstand something fundamental?
Any pointers are greatly appreciated.