Google+ Peter Bromberg's .NET Blog | ASP.NET

Peter Bromberg's .NET Blog All Things Programming

Build an Indeed Job Search Engine

5. September 2016 15:59 by admin in API, ASP.NET

A common task for developers is  - you guessed it – to search for a job! This post will show you how to build a standard ASP.NET App that uses the Indeed API to make searching for jobs easy.  I’ll show the basics of how I constructed the app and included will be a download with the complete Visual Studio 2015 source code. The only thing you’ll need is a new Indeed publisher API key to insert into the AppSettings section of your web.config, and you should start out by getting that here:

http://www.indeed.com/publisher

The Indeed API is a very simple, REST – based API that returns XML. So we’ll have two classes for our search interface – the result class, and the QueryParameters class, which holds the various query parameters that can be passed on the querystring of the API URL.

Here’s the QueryParameters class first:

public class IndeedQueryParameters

    {

        /// <summary>

        /// Searches Job Title.

        /// </summary>

        public string JobQuery { get; set; }

       

        /// <summary>

        /// Searches for jobs near a particular location.

        /// </summary>

        public string Location { get; set; }

        /// <summary>

        /// Sort by relevance or date. Default is relevance.

        /// </summary>

        public string Sort { get; set; }

        /// <summary>

        /// Distance from search location ("as the crow flies"). Default is 25.

        /// </summary>

        public string SearchRadius { get; set; }

        /// <summary>

        /// Site type. To show only jobs from job boards use 'jobsite'. For jobs from direct employer websites use 'employer'.

        /// </summary>

        public string St { get; set; }

        /// <summary>

        /// Job type. Allowed values: "fulltime", "parttime", "contract", "internship", "temporary".

        /// </summary>

        public string Jt { get; set; }

        /// <summary>

        /// Start results at this result number, beginning with 0. Default is 0.

        /// </summary>

        public string Start { get; set; }

        /// <summary>

        /// Maximum number of results returned per query. Default is 10

        /// </summary>

        public string Limit { get; set; }

        /// <summary>

        /// Number of days back to search.

        /// </summary>

        public string FromAge { get; set; }

        /// <summary>

        /// Filter duplicate results. 0 turns off duplicate job filtering. Default is 1.

        /// </summary>

        public string Filter { get; set; }

        /// <summary>

        /// If latlong=1, returns latitude and longitude information for each job result. Default is 0.

        /// </summary>

        public string LatitudeLongitude { get; set; }

        /// <summary>

        /// Search within country specified. Default is us. See below for a complete list of supported countries.

        /// </summary>

        public string Country { get; set; }

        /// <summary>

        /// Channel Name: Group API requests to a specific channel.

        /// </summary>

        public string Channel { get; set; }

        /// <summary>

        /// The IP number of the end-user to whom the job results will be displayed. This field is required.

        /// </summary>

        public string UserIP { get; set; }

        /// <summary>

        /// The User-Agent (browser) of the end-user to

        /// whom the job results will be displayed.

        /// This can be obtained from the "User-Agent"

        /// HTTP request header from the end-user.

        /// This field is required.

        /// </summary>

        public string UserAgent { get; set; }

    }

And the IndeedSearchResult class which maps to the returned XML document:

public class IndeedSearchResult
{
     /// <summary>
     /// The time the job listing was posted formatted
     /// for display.
     /// </summary>
     /// <example>6 days ago</example>
     public string FormattedRelativeTime { get; set; }

     /// <summary>
     /// Has this job listing expired?
     /// </summary>
     /// <example>false</example>
     public string Expired { get; set; }

     /// <summary>
     /// Is this an Indeed sponsored job?
     /// </summary>
     /// <example>false</example>
     public string Sponsored { get; set; }

     /// <summary>
     /// The indeed job key.
     /// </summary>
     /// <example>0123456789abcdef</example>
     public string JobKey { get; set; }

     /// <summary>
     /// Latitude of the job's location.
     /// </summary>
     /// <example>41.057693</example>
     public string Latitude { get; set; }

     /// <summary>
     /// Longitude of the job's location.
     /// </summary>
     /// <example>-73.54395</example>
     public string Longitude { get; set; }

     /// <summary>
     /// Code to be placed on the onMouseDown event of the title link.
     /// </summary>
     /// <example>indeed_clk(this, '');</example>
     public string OnMouseDown { get; set; }

     /// <summary>
     /// A short description of the summary of the job.
     /// </summary>
     /// <example>Indeed is looking for strategic Account Executives to help in the expansion of our Stamford, CT location. We are
     /// in the process of interviewing sales candidates who have 2-5 years of sales experience and who have experience
     /// generating new business and growing existing accounts. A strong candidate will have excellent communication
     /// skills, consistent work ethic and a desire to be a part of the...</example>
     public string Snippet { get; set; }

     /// <summary>
     /// The date that the job was posted.
     /// </summary>
     /// <example>Tue, 03 Aug 2010 14:00:47 GMT</example>
     public string Date { get; set; }

     /// <summary>
     /// The country where the job is located.  This
     /// will be in the format of a country code.
     /// </summary>
     /// <example>US</example>
     public string Country { get; set; }

     /// <summary>
     /// The state where the job is located.
     /// </summary>
     /// <example>CT</example>
     public string State { get; set; }

     /// <summary>
     /// The source of the job posting.
     /// </summary>
     /// <example>Indeed</example>
     public string Source { get; set; }

     /// <summary>
     /// The city where the job is located.
     /// </summary>
     /// <example>Stamford</example>
     public string City { get; set; }

     /// <summary>
     /// The job title.
     /// </summary>
     /// <example>Sales Account Executive</example>
     public string JobTitle { get; set; }

     /// <summary>
     /// The company the job is for.
     /// </summary>
     /// <example>Indeed</example>
     public string Company { get; set; }

     /// <summary>
     /// Full location of the job listing that is
     /// formatted for display.
     /// </summary>
     /// <example>Stamford, CT 06902</example>
     public string FormattedLocationFull { get; set; }

     /// <summary>
     /// The URL of the job posting on Indeed.com
     /// </summary>
     /// <example>http://www.indeed.com/rc/clk?jk=0123456789abcdef</example>
     /// <remarks>Notice that the job key is part of the URL.</remarks>
     public string URL { get; set; }
}

With these items complete, all we need is our search method so that we can bind results to a standard ASP.NET DataGrid:

public static List<IndeedSearchResult> GetSearchResults(IndeedQueryParameters parameters, string apiPublisherKey)
      {
          Contract.Requires(null != apiPublisherKey);
          Contract.Requires("" != apiPublisherKey.Trim());
          // To get your own Publisher ID:  http://www.indeed.com/publisher

          string requestUrl = "http://api.indeed.com/ads/apisearch" +
                                          String.Format("?publisher={0}", apiPublisherKey) +
                                          String.Format("&q={0}", parameters.JobQuery) +
                                          String.Format("&l={0}", parameters.Location) +
                                          String.Format("&sort={0}", parameters.Sort) +
                                          String.Format("&radius={0}", parameters.SearchRadius) +
                                          String.Format("&st={0}", parameters.St) +
                                          String.Format("&jt={0}", parameters.Jt) +
                                          String.Format("&start={0}", parameters.Start) +
                                          String.Format("&limit={0}", parameters.Limit) +
                                          String.Format("&fromage={0}", parameters.FromAge) +
                                          String.Format("&filter={0}", parameters.Filter) +
                                          String.Format("&latlong={0}", parameters.LatitudeLongitude) +
                                          String.Format("&co={0}", parameters.Country) +
                                          String.Format("&chnl={0}", parameters.Channel) +
                                          String.Format("&userip={0}", parameters.UserIP) +
                                          String.Format("&useragent={0}", parameters.UserAgent) +
                                          "&v=2";

          XmlDocument doc = new XmlDocument();
          doc.Load(requestUrl);
          XmlElement root = doc.DocumentElement;
          XmlNodeList nodes = root.SelectNodes("//results//result");

          List<IndeedSearchResult> results = new List<IndeedSearchResult>();
          foreach (XmlNode node in nodes)
          {
              IndeedSearchResult result = new IndeedSearchResult();
              result.JobTitle = node["jobtitle"].InnerText;
              result.Company = node["company"].InnerText;
              result.City = node["city"].InnerText;
              result.State = node["state"].InnerText;
              result.Country = node["country"].InnerText;
              result.FormattedLocationFull = node["formattedLocation"].InnerText;
              result.Source = node["source"].InnerText;
              result.Date =DateTime.Parse( node["date"].InnerText).ToShortDateString();
              result.Snippet = node["snippet"].InnerText;
              result.URL = node["url"].InnerText;
              result.OnMouseDown = node["onmousedown"].InnerText;
            
                
                 if ( node["latitude"] !=null)  // prevent blow-ups if null
              result.Latitude = node["latitude"].InnerText;
              result.JobKey = node["jobkey"].InnerText;
              result.Sponsored = node["sponsored"].InnerText;
              result.Expired = node["expired"].InnerText;
              result.FormattedLocationFull = node["formattedLocationFull"].InnerText;
              result.FormattedRelativeTime = node["formattedRelativeTime"].InnerText;

              results.Add(result);
          }
          results = results.OrderByDescending(x => x.Date).ToList();
          return results;
      }

The final step is to perform a search based on the minimum number of QueryParameter inputs, namely the job search term e.g. “.NET Developer” and the location, e.g. “Orlando FL” and we’re done:

protected void DoSearch(int pagestart = 0)
       {
           Indeed.IndeedSearch srch = new IndeedSearch();
           IndeedQueryParameters parms = new IndeedQueryParameters();
           parms.Start = pagestart.ToString();
           parms.JobQuery = txtSearch.Text;
           parms.Location = txtLocation.Text;
           parms.Limit = 1000.ToString();
           parms.UserIP = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()).AddressList[1].ToStrin‌​g();


           List<IndeedSearchResult> results = srch.GetSearchResults(parms);
           GridView1.PageIndex = pagestart;
           GridView1.DataSource = results;
           GridView1.DataBind();

       }

 

I’ve added paging and sorting by job date.  You can download the complete source code here:

IndeedWeb.zip (10.95 mb)

How to Ensure that jQuery is only loaded once

26. July 2016 13:50 by admin in ASP.NET, C#, Jquery

Often we have pages and master pages and "bundles" that load scripts like jQuery and we can't always be sure that a particular page already has it loaded. Here is a script that will only inject jQuery into a page if it has not already been loaded:

<HTML>
<HEAD>

<!-- comment or uncomment next line to test it out --->
<script src='https://code.jquery.com/jquery-3.1.0.min.js'></script>

<script>

// Only do anything if jQuery isn't defined

if (typeof jQuery == 'undefined') {

function getScript(url, success) {

var script = document.createElement('script');

    script.src = url;

var head = document.getElementsByTagName('head')[0],done = false;

// Attach handlers for all browsers

script.onload = script.onreadystatechange = function() {

if (!done && (!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete')) {

done = true;

// callback function provided as param

success();

script.onload = script.onreadystatechange = null;

head.removeChild(script);

};

};

head.appendChild(script);

};

 

getScript('https://code.jquery.com/jquery-3.1.0.min.js', function() {

if (typeof jQuery=='undefined') {

// Super failsafe

} else {

alert("We Loaded it!");

// jQuery loaded 

}

});

 

} else { 

// jQuery was already loaded

alert("jQuery is Already Loaded!");

// Run your jQuery Code

};

</script>

</HEAD>

<BODY>

</BODY>

 

</HTML>

 

Implementing a Custom IPrincipal in an ASP.NET MVC Application

25. January 2014 14:45 by admin in ASP.NET, C#, MVC

 I have an MVC application for which I have implemented a Custom ExtendedMembership - derived Membership class that is hosted via a WCF Service.

I have a custom MembershipProviderForwarder class that plugs right into my web.config as the Membership provider, but what it actually does is forward all Membership calls to my WCF Service, which uses the real custom Membershp provider, and returns back all the results to the app from over the wire.

In this manner I can have any number of MVC apps all using the same provider via my Webservice.

Since I have custom Membership fields, but only a couple, I didn't want to get into writing a lot of Profile code. Instead, I have a Custom User object that has these extra fields which can be returned from the UserProfile table in SQL Server.

The issue is that I only want to make Webservice calls when the user first logs in, and I want to store my custom fields in the Forms Ticket (it has a UserData property for just this purpose). In this manner I can use the PostAuthenticateRequest event to pull my custom data our of the forms cookie and attach my custom IPrincipal to the HttpContext.Current.User property for each subsequent request. Here is how I did that:

 

1. Create the interface

 

interface ICustomPrincipal : IPrincipal

{

    int UserId { get; set; }

    string FirstName { get; set; }

    string LastName { get; set; }

}

 

2. CustomPrincipal

 

public class CustomPrincipal : ICustomPrincipal

{

    public IIdentity Identity { get; private set; }

    public bool IsInRole(string role) { return false; }

 

    public CustomPrincipal(string email)

    {

        this.Identity = new GenericIdentity(email);

    }

 

    public int UserId { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

}

 

3. CustomPrincipalSerializeModel - for serializing custom information into userdata field in FormsAuthenticationTicket object.

 

public class CustomPrincipalSerializeModel

{

    public int UserId { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

}

 

4. LogIn method - setting up a cookie with custom information

 

if (Membership.ValidateUser(viewModel.Email, viewModel.Password))

{

    var user = userRepository.Users.Where(u => u.Email == viewModel.Email).First();

 

    CustomPrincipalSerializeModel serializeModel = new CustomPrincipalSerializeModel();

    serializeModel.UserId = user.Id;

    serializeModel.FirstName = user.FirstName;

    serializeModel.LastName = user.LastName;

 

    JavaScriptSerializer serializer = new JavaScriptSerializer();

 

    string userData = serializer.Serialize(serializeModel);

 

    FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(

             1,

             viewModel.Email,

             DateTime.Now,

             DateTime.Now.AddMinutes(15),

             false,

             userData);

 

    string encTicket = FormsAuthentication.Encrypt(authTicket);

    HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);

    Response.Cookies.Add(faCookie);

 

    return RedirectToAction("Index", "Home");

}

 

5. Global.asax.cs - Reading cookie and replacing HttpContext.User object, this is done by overriding PostAuthenticateRequest

 

protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)

{

    HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];

    if (authCookie != null)

    {

        FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);

        JavaScriptSerializer serializer = new JavaScriptSerializer();

        CustomPrincipalSerializeModel serializeModel = serializer.Deserialize<CustomPrincipalSerializeModel>(authTicket.UserData);

        CustomPrincipal newUser = new CustomPrincipal(authTicket.Name);

        newUser.UserId = serializeModel.UserId;

        newUser.FirstName = serializeModel.FirstName;

        newUser.LastName = serializeModel.LastName;

        HttpContext.Current.User = newUser;

    }

}

 

 

6. Access in Razor views

 

@((User as CustomPrincipal).Id)

@((User as CustomPrincipal).FirstName)

@((User as CustomPrincipal).LastName)

 

and in code:

 

    (User as CustomPrincipal).Id

    (User as CustomPrincipal).FirstName

    (User as CustomPrincipal).LastName

Display RSS Feed with JSONP and AJAX UI-blocking Loader

2. July 2013 08:38 by admin in ASP.NET, JSON

Here is a technique to display an RSS feed with the jQuery $ajax method, using Yahoo's YQL query API to retrieve the feed and return it as JSON:

<!DOCTYPE HTML>
<html>
 <head>
<title> Feed Demo with Ajax Loader </title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="Scripts/jquery.blockUI.js"></script>
  <script>
function stopUI() {
    $.blockUI({ message: '<img id="AjaxLoader" src="Images/ajax-loader.gif" />',
        css: {
            padding: '25px',
            margin: 0,
            width: '5%',
            top: '40%',
            left: '40%',
            color: '#000',
            textAlign: 'center',
            border: '3px solid #aaa',
            backgroundColor: '#efefef',
            cursor: 'wait'
        },
        overlayCSS: {
            backgroundColor: '#000',
            opacity: 0.2
        }
    });
    $('body').css('cursor', 'wait');
}
 
// REMOVE THE BLOCKED INTERFACE
function startUI() {
    $.unblockUI();
    $('body').css('cursor', 'default');
}
 
$(function() {
    stopUI();
     getFeed();
});
 
function getFeed()
{ 
    $.ajax({
// ajax request to yql public json url 
      url : 'http://query.yahooapis.com/v1/public/yql',
      jsonp : 'callback',
// tell jQuery that we need JSON format
      dataType : 'jsonp',
// tell YQL that what we want and to output in JSON format
      data : {
          q : 'select * from rss where url ="http://www.peterbromberg.net/syndication.axd"',
          format : 'json'
      },
// parse response data
      success : function(data) {
        $.each(data.query.results.item, function(i,data)
        {
var news_data =
"<div><a href='"+data.link+"'>" + data.title + "</a>" + 
          data.description +"</div>";
          $(news_data).appendTo("#news");
        });
 
      }    ,
      complete: function(){
          startUI();
          } 
    });
 
}
</script>
 </head>
 <body>
<div id="news"></div>
 </body>
</html>


The download includes the Ajax-loader image as well as the required jquery.blockUI.js javascript.


 

blockUi.zip (9.58 kb)