OData – The easiest service I can create: now with updates
- by Jon Dalberg
The other day I created a simple NastyWord service exposed via OData. It was read-only and used an in-memory backing store for the words. Today I’ll modify it to use a file instead of a list and I’ll accept new nasty words by implementing IUpdatable directly. The first thing to do is enable the service to accept new entries. This is done at configuration time by adding the “WriteAppend” access rule: 1: public class NastyWords : DataService<NastyWordsDataSource>
2: {
3: // This method is called only once to initialize service-wide policies.
4: public static void InitializeService(DataServiceConfiguration config)
5: {
6: config.SetEntitySetAccessRule("*", EntitySetRights.AllRead | EntitySetRights.WriteAppend);
7: config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
8: }
9: }
Next I placed a file, NastyWords.txt, in the “App_Data” folder and added a few *choice* words to start. This required one simple change to our NastyWordDataSource.cs file:
1: public NastyWordsDataSource()
2: {
3: UpdateFromSource();
4: }
5:
6: private void UpdateFromSource()
7: {
8: var words = File.ReadAllLines(pathToFile);
9: NastyWords = (from w in words
10: select new NastyWord { Word = w }).AsQueryable();
11: }
Nothing too shocking here, just reading each line from the NastyWords.txt file and exposing them. Next, I implemented IUpdatable which comes with a boat-load of methods. We don’t need all of them for now since we are only concerned with allowing new values. Here are the methods we must implement, all the others throw a NotImplementedException:
1: public object CreateResource(string containerName, string fullTypeName)
2: {
3: var nastyWord = new NastyWord();
4: pendingUpdates.Add(nastyWord);
5: return nastyWord;
6: }
7:
8: public object ResolveResource(object resource)
9: {
10: return resource;
11: }
12:
13: public void SaveChanges()
14: {
15: var intersect = (from w in pendingUpdates
16: select w.Word).Intersect(from n in NastyWords
17: select n.Word);
18:
19: if (intersect.Count() > 0)
20: throw new DataServiceException(500, "duplicate entry");
21:
22: var lines = from w in pendingUpdates
23: select w.Word;
24:
25: File.AppendAllLines(pathToFile,
26: lines,
27: Encoding.UTF8);
28:
29: pendingUpdates.Clear();
30:
31: UpdateFromSource();
32: }
33:
34: public void SetValue(object targetResource, string propertyName, object propertyValue)
35: {
36: targetResource.GetType().GetProperty(propertyName).SetValue(targetResource, propertyValue, null);
37: }
I use a simple list to contain the pending updates and only commit them when the “SaveChanges” method is called. Here’s the order these methods are called in our service during an insert:
CreateResource – here we just instantiate a new NastyWord and stick a reference to it in our pending updates list.
SetValue – this is where the “Word” property of the NastyWord instance is set.
SaveChanges – get the list of pending updates, barfing on duplicates, write them to the file and clear our pending list.
ResolveResource – the newly created resource will be returned directly here since we aren’t dealing with “handles” to objects but the actual objects themselves.
Not too bad, eh? I didn’t find this documented anywhere but a little bit of digging in the OData spec and use of Fiddler made it pretty easy to figure out. Here is some client code which would add a new nasty word:
1: static void Main(string[] args)
2: {
3: var svc = new ServiceReference1.NastyWordsDataSource(new Uri("http://localhost.:60921/NastyWords.svc"));
4: svc.AddToNastyWords(new ServiceReference1.NastyWord() { Word = "shat" });
5:
6: svc.SaveChanges();
7: }
Here’s all of the code so far for to implement the service:
1: using System;
2: using System.Collections.Generic;
3: using System.Data.Services;
4: using System.Data.Services.Common;
5: using System.Linq;
6: using System.ServiceModel.Web;
7: using System.Web;
8: using System.IO;
9: using System.Text;
10:
11: namespace ONasty
12: {
13: [DataServiceKey("Word")]
14: public class NastyWord
15: {
16: public string Word { get; set; }
17: }
18:
19: public class NastyWordsDataSource : IUpdatable
20: {
21: private List<NastyWord> pendingUpdates = new List<NastyWord>();
22: private string pathToFile = @"path to your\App_Data\NastyWords.txt";
23:
24: public NastyWordsDataSource()
25: {
26: UpdateFromSource();
27: }
28:
29: private void UpdateFromSource()
30: {
31: var words = File.ReadAllLines(pathToFile);
32: NastyWords = (from w in words
33: select new NastyWord { Word = w }).AsQueryable();
34: }
35:
36: public IQueryable<NastyWord> NastyWords { get; private set; }
37:
38: public void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded)
39: {
40: throw new NotImplementedException();
41: }
42:
43: public void ClearChanges()
44: {
45: pendingUpdates.Clear();
46: }
47:
48: public object CreateResource(string containerName, string fullTypeName)
49: {
50: var nastyWord = new NastyWord();
51: pendingUpdates.Add(nastyWord);
52: return nastyWord;
53: }
54:
55: public void DeleteResource(object targetResource)
56: {
57: throw new NotImplementedException();
58: }
59:
60: public object GetResource(IQueryable query, string fullTypeName)
61: {
62: throw new NotImplementedException();
63: }
64:
65: public object GetValue(object targetResource, string propertyName)
66: {
67: throw new NotImplementedException();
68: }
69:
70: public void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved)
71: {
72: throw new NotImplementedException();
73: }
74:
75: public object ResetResource(object resource)
76: {
77: throw new NotImplementedException();
78: }
79:
80: public object ResolveResource(object resource)
81: {
82: return resource;
83: }
84:
85: public void SaveChanges()
86: {
87: var intersect = (from w in pendingUpdates
88: select w.Word).Intersect(from n in NastyWords
89: select n.Word);
90:
91: if (intersect.Count() > 0)
92: throw new DataServiceException(500, "duplicate entry");
93:
94: var lines = from w in pendingUpdates
95: select w.Word;
96:
97: File.AppendAllLines(pathToFile,
98: lines,
99: Encoding.UTF8);
100:
101: pendingUpdates.Clear();
102:
103: UpdateFromSource();
104: }
105:
106: public void SetReference(object targetResource, string propertyName, object propertyValue)
107: {
108: throw new NotImplementedException();
109: }
110:
111: public void SetValue(object targetResource, string propertyName, object propertyValue)
112: {
113: targetResource.GetType().GetProperty(propertyName).SetValue(targetResource, propertyValue, null);
114: }
115: }
116:
117: public class NastyWords : DataService<NastyWordsDataSource>
118: {
119: // This method is called only once to initialize service-wide policies.
120: public static void InitializeService(DataServiceConfiguration config)
121: {
122: config.SetEntitySetAccessRule("*", EntitySetRights.AllRead | EntitySetRights.WriteAppend);
123: config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
124: }
125: }
126: }
Next time we’ll allow removing nasty words. Enjoy!