Skip to content

Commit d78f5d6

Browse files
committed
Blog Post - Replacing the mega postback
1 parent fef3db5 commit d78f5d6

File tree

2 files changed

+309
-0
lines changed

2 files changed

+309
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
#Replacing the Mega Postback with Web Api
2+
There is a pattern that can be found in a few places in BugTracker.NET. I'm not sure what to call it other than the mega postback. These are aspx pages that contain no view. The page consists entirely of code in the page load method, following this pattern:
3+
4+
```
5+
public partial class mega_postback : BasePage
6+
{
7+
8+
public void Page_Load(Object sender, EventArgs e)
9+
{
10+
if (IsPostback)
11+
{
12+
//Business Logic handling the post request
13+
}
14+
}
15+
}
16+
```
17+
A great example of this in is the code-behind for the insert_bug.aspx page which I have abbreviated here. You can see the full version [here](https://github.com/dpaquette/BugTracker.NET/blob/fef3db538ed359fd60e1d6ce18976a5f75f61c16/src/BugTracker.Web/insert_bug.aspx.cs).
18+
```
19+
public partial class insert_bug : BasePage
20+
{
21+
public void Page_Load(Object sender, EventArgs e)
22+
{
23+
Util.set_context(HttpContext.Current);
24+
Util.do_not_cache(Response);
25+
26+
string username = Request["username"];
27+
string password = Request["password"];
28+
string projectid_string = Request["projectid"];
29+
string comment = Request["comment"];
30+
//...get and validate all the other parameters
31+
32+
// authenticate user
33+
bool authenticated = Authenticate.check_password(username, password);
34+
35+
if (!authenticated)
36+
{
37+
Response.AddHeader("BTNET", "ERROR: invalid username or password");
38+
Response.Write("ERROR: invalid username or password");
39+
Response.End();
40+
}
41+
42+
//..300 lines of code related to inserting a bug and sending notificaitons
43+
44+
Response.AddHeader("BTNET", "OK:" + Convert.ToString(bugid));
45+
Response.Write("OK:" + Convert.ToString(bugid));
46+
Response.End();
47+
}
48+
}
49+
50+
```
51+
52+
The aspx page itself is empty. All code paths for the Page_Load method above write either "OK" or "ERROR" to the response and call Response.End(). This page is called from the POP3 integration process that creates bugs from incoming email bug reports:
53+
```
54+
//...Create the URL and message body based on incoming email
55+
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(Url);
56+
req.Credentials = CredentialCache.DefaultCredentials;
57+
req.PreAuthenticate = true;
58+
req.Method = "POST";
59+
req.ContentType = "application/x-www-form-urlencoded";
60+
req.ContentLength = bytes.Length;
61+
Stream request_stream = req.GetRequestStream();
62+
request_stream.Write(bytes, 0, bytes.Length);
63+
request_stream.Close();
64+
res = (HttpWebResponse)req.GetResponse();
65+
//...Check the response for OK vs ERROR message
66+
```
67+
The logic around this is more complex than it needs to be. With Web API, we have a much easier approach to creating an HTTP api that will feel much more standard than the attempt to create an API using the mega-postback pattern.
68+
##Adding a Web API controller
69+
Luckily, we added Web API back when we updated the grid components so adding a new controller is easy. Right click the Controllers folder and select Add-> Controller. Select the Web Api Controller - Empty template and name the controller BugController.
70+
We will add the `Authorize` attribute to the BugController to ensure that only aunthenticated users can acess the endpoints exposed by this controller. Adding this simple attribute will replace a good 30-40 lines of code from the insert_bug.aspx code. The authentication step will be handled separately by the caller as we will see later.
71+
Next, we will add a placeholder for the Post method. This method will be accessed by the API client by sending a POST request to api/Bug.
72+
```
73+
[Authorize]
74+
public class BugController : ApiController
75+
{
76+
[HttpPost]
77+
public IHttpActionResult Post()
78+
{
79+
return Ok();
80+
}
81+
}
82+
```
83+
[View the commit](https://github.com/dpaquette/BugTracker.NET/commit/a33da4140a8a0c7e99a1212a116835141c3d2a40) - Add Bugs Controller
84+
85+
##Model Class and Validation
86+
In the mega post-back, request parameters are accessed via the Request indexer.
87+
```
88+
string username = Request["username"];
89+
string password = Request["password"];
90+
string projectid_string = Request["projectid"];
91+
string comment = Request["comment"];
92+
string from_addr = Request["from"];
93+
string cc = "";
94+
string message = Request["message"];
95+
string attachment_as_base64 = Request["attachment"];
96+
string attachment_content_type = Request["attachment_content_type"];
97+
string attachment_filename = Request["attachment_filename"];
98+
string attachment_desc = Request["attachment_desc"];
99+
string bugid_string = Request["bugid"];
100+
string short_desc = Request["short_desc"];
101+
```
102+
When using Web API, we can create a class that contains all the parameters from the request. Since the client will already be logged in, we don't need the username and password to be part of the request. The user information can be access from the User property of the base ApiController.
103+
104+
The new BugFromEmail model class looks like this:
105+
```
106+
public class BugFromEmail
107+
{
108+
public int? ProjectId { get; set; }
109+
public string Comment { get; set; }
110+
111+
[Required]
112+
public string FromAddress { get; set; }
113+
public string CcAddress { get; set; }
114+
public string Message { get; set; }
115+
public byte[] Attachment { get; set; }
116+
public string AttachmentContentType { get; set; }
117+
public string AttachmentFileName { get; set; }
118+
public string AttachmentDescription { get; set; }
119+
public int? BugId { get; set; }
120+
public string ShortDescription { get; set; }
121+
}
122+
```
123+
Notice that the FromAddress is marked as Required using a data annoation attribute. This will allow us to use model state validation in the controller action. Strangely, the FromAddress is the only item that is required for the create bug request in BugTracker. All other properties fall back to defaults.
124+
125+
The controller Post method will accept an instance of the BugFromEmail class which will be mapped automatically by Web API from the body of the reqest. After checking if the model state is valid, it can continue with processing the request. If not, then a BadRequest (HTTP 400) result is returned. Note that we will rely on the standard HTTP response codes instead of a text based OK / ERROR result.
126+
127+
```
128+
[Authorize]
129+
public class BugFromEmailController : ApiController
130+
{
131+
[HttpPost]
132+
public IHttpActionResult Post([FromBody] BugFromEmail bugFromEmail)
133+
{
134+
if (ModelState.IsValid)
135+
{
136+
//Insert the bug
137+
return Ok();
138+
}
139+
else
140+
{
141+
return BadRequest(ModelState);
142+
}
143+
144+
}
145+
}
146+
```
147+
##Adding the logic
148+
As a first pass, we can move the code from insert_bug.aspx to the Post method. Simply by moving to Web API, we can make eliminate some code that is handled for us by Web API. One example is thhe following code for logging in is no longer necessary.
149+
150+
```
151+
if (username == null
152+
|| username == "")
153+
{
154+
Response.AddHeader("BTNET", "ERROR: username required");
155+
Response.Write("ERROR: username required");
156+
Response.End();
157+
}
158+
159+
if (password == null
160+
|| password == "")
161+
{
162+
Response.AddHeader("BTNET", "ERROR: password required");
163+
Response.Write("ERROR: password required");
164+
Response.End();
165+
}
166+
167+
// authenticate user
168+
169+
bool authenticated = Authenticate.check_password(username, password);
170+
171+
if (!authenticated)
172+
{
173+
Response.AddHeader("BTNET", "ERROR: invalid username or password");
174+
Response.Write("ERROR: invalid username or password");
175+
Response.End();
176+
}
177+
IIdentity identity = Security.Security.GetIdentity(username);
178+
```
179+
180+
The Authorize attribute already ensure that only authenticated clients will be able to call this method. If the client are not authenticated, Web API will return an appropriate response code. Since the client is authenticed, we can get the IIdentity instance the same way we do every where else in the application:
181+
182+
```
183+
IIdentity = User.Identity;
184+
```
185+
Likewise, code for parsing request parameteres is no longer needed since this is handled automatically by the Web API model binder:
186+
```
187+
string projectid_string = Request["projectid"];
188+
int projectid = 0;
189+
if (Util.is_int(projectid_string))
190+
{
191+
projectid = Convert.ToInt32(projectid_string);
192+
}
193+
```
194+
Finally, the responses are also simplified since we no longer need mannualy write the response text:
195+
```
196+
Response.AddHeader("BTNET", "OK:" + Convert.ToString(new_ids.bugid));
197+
Response.Write("OK:" + Convert.ToString(new_ids.bugid));
198+
Response.End();
199+
```
200+
Instead, we can use a standard 200 OK response in Web API:
201+
```
202+
return Ok(newIds.bugid);
203+
```
204+
The code for this web api method is still a more complex than I would like it to be, but it is a big improvement from what we started with. We were able to eliminate ~90 lines from a 340 line method. Since the new code is a little easier to understand, we are also in a much better position to refactor it going forward.
205+
206+
[View the Commit - Simple move of Insert_bug.aspx to Web API](https://github.com/dpaquette/BugTracker.NET/commit/abf82b5edb46dc51be5278419a882e33569ec6fe)
207+
208+
##Supporting Client Login
209+
We could attempt to post to the existing login page to login as a client but that would make for some confusing code. Instead, I extracted the login code to a class that can be used by the login page and by a new login Web API controller.
210+
211+
The controller itself is very simple:
212+
213+
```
214+
public class LoginController : ApiController
215+
{
216+
[HttpPost]
217+
public IHttpActionResult Post(LoginModel loginModel)
218+
{
219+
LoginResult loginResult = Authenticate.AttemptLogin(Request.GetOwinContext(), loginModel.User, loginModel.Password);
220+
221+
if (loginResult.Success)
222+
{
223+
return Ok();
224+
}
225+
else
226+
{
227+
return StatusCode(HttpStatusCode.Forbidden);
228+
}
229+
}
230+
}
231+
232+
public class LoginModel
233+
{
234+
public string User { get; set; }
235+
236+
public string Password { get; set; }
237+
}
238+
```
239+
240+
Testing this endpoint in Fidler shows the 200 OK result on successful login as well as the Set-Cookie header for the authentication cookie. It will be the client's responsibility to include this header in subsequent requests.
241+
242+
![Testing Web API Login](Images/TestingWebAPILogin.png)
243+
244+
245+
[View the commit - Refactored Login logic to support web api login]()
246+
247+
##Updating the Client
248+
Now, the client will need to change a little. We changed the URL for posting bug from email and we also changed the way we authenticate. Instead of passing the username/password with the request, we will need to login as a separate request.
249+
250+
While we are making changes to the client here, we should also upgrade to a more modern library for making HTTP Requests. The current recommended library for making HTTP requests is Http Client.
251+
252+
```
253+
Install-Package Microsoft.Net.Http
254+
```
255+
256+
We can simplify the error logic a lot by using the EnsureSuccessStatusCode() method on the response object. This will through an exception if the status code is anything other than a 200 OK.
257+
258+
```
259+
using (var httpClient = new HttpClient(handler))
260+
{
261+
var loginParameters = new Dictionary<string, string>
262+
{
263+
{ "user", ServiceUsername },
264+
{ "password", ServicePassword }
265+
};
266+
267+
HttpContent loginContent = new FormUrlEncodedContent(loginParameters);
268+
var loginResponse = await httpClient.PostAsync(LoginUrl, loginContent);
269+
loginResponse.EnsureSuccessStatusCode();
270+
271+
string rawMessage = Encoding.Default.GetString(mimeMessage.RawMessage);
272+
var postBugParameters = new Dictionary<string, string>
273+
{
274+
{ "projectId", Convert.ToString(projectid) },
275+
{ "fromAddress", from },
276+
{ "shortDescription", subject},
277+
{ "message", rawMessage}
278+
//Any other paramters go here
279+
};
280+
if (useBugId)
281+
{
282+
postBugParameters.Add("bugId", bugidString);
283+
}
284+
285+
HttpContent bugContent = new FormUrlEncodedContent(postBugParameters);
286+
var postBugResponse = await httpClient.PostAsync(InsertBugUrl, bugContent);
287+
288+
postBugResponse.EnsureSuccessStatusCode();
289+
}
290+
if (MessageInputFile == "" && DeleteMessagesOnServer == "1")
291+
{
292+
write_line("sending POP3 command DELE");
293+
client.DeleteMessage(message_number);
294+
}
295+
}
296+
catch (Exception e)
297+
{
298+
write_line("HttpWebRequest error url=" + InsertBugUrl);
299+
write_line(e);
300+
write_line("Incrementing total error count");
301+
total_error_count++;
302+
}
303+
304+
```
305+
306+
[View the commit - Update btnetservice to use HttpClient and new Web API endpoints](https://github.com/dpaquette/BugTracker.NET/commit/f77366f74e4b34892a963a9e6b6313fe6ae0f242)
307+
308+
##Conclusion
309+
We have shown how moving from a _mega post back_ to a more standard Web API can help to simplify our code. While more refactoring can be done to improve the implementation further, we have acheived our goal of replacing the _mega post back_ with an implementation that is easier to understand and much easier to test.

Images/TestingWebAPILogin.png

35 KB
Loading

0 commit comments

Comments
 (0)