|
| 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 | + |
| 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. |
0 commit comments