Find your US Representatives and Congressional Districts with SMS and ASP.NET Core
Niels Swimburger.NET π
Posted on August 4, 2022
This blog post was written for Twilio and originally published at the Twilio blog.
When someone runs for office in the United States of America (U.S.), they have to fulfill certain "Ballot Access Requirements" to become listed on the ballot. One of those requirements could be that you need a certain number of signatures per district. So when volunteers and campaign workers go out to gather signatures, it is instrumental to know which congressional district (CD) the signee belongs to. However, knowing which congressional district you are from is not common knowledge, especially in areas where the district cuts right through counties, cities, and neighborhoods.
States redraw their congressional districts every decade after getting an updated population count from the Census Bureau. Each district is represented by a single U.S. representative in the U.S. House of Representatives. However, the manner in which these districts are drawn often result in unique and odd shapes which can be confusing. This is why constituents living just a street apart sometimes live in different districts, and as a result, vote in different house races.
This blog post won't discuss why U.S. congressional district maps are drawn the way they are, however, I highly recommend you look into this topic and learn how it affects political voting power.
When I gathered signatures, the first question I had to ask over and over again was "what congressional district are you from?", and 95% of the time, the answer was "I don't know". That's because in Northern Virginia, the border between the 10th and 11th congressional district cuts directly through some counties, cities, neighborhoods, and streets.
Map of Virginia's 11th Congressional District
The house.gov website has a tool to look up your congressional district and representative, but unfortunately, the tool is slow and painful to use, especially when you're on the go on mobile internet.
That's why instead of using web technology, I built a solution using connectivity technology that's more accessible, more reliable, and simpler. Using Twilio SMS, I built a phone number that you can text your address to and you will receive a response with your congressional district and representative. Let me walk you through how you can build this SMS bot using Twilio, C# .NET, and Google's Civic Information API.
Prerequisites
You'll need a couple of things to follow along:
- .NET 6 SDK (older or newer should work too)
- A code editor or IDE (I recommend VS Code with the C# plugin, Visual Studio, or JetBrains Rider)
- The ngrok CLI, and optionally, a free ngrok account
- A Twilio account (If you register for a Twilio account here, you'll receive $10 in Twilio credit when you upgrade to a paid account!)
- A Twilio Phone Number
- A personal phone or the Twilio Dev Phone to test your bot
- A free Google Cloud account
You can find the source code for this tutorial on GitHub. Use it as a reference if you run into any issues, or submit an issue if you need assistance.
Get Started with the Google Civic Information API
This SMS bot will use the Google Civic Information API to look up the U.S. congressional districts and representatives. This API has a free tier as part of the Google Cloud Platform.
Go to the Google Cloud Console and create a new project. Once the project is created, select the project using the project dropdown in the top-left navigation bar.
You'll first need to enable the Civic Information API. Click on "ENABLE APIS AND SERVICES", then search for the " Civic Information API".
Click on the search result and then click ENABLE.
Now that the API is enabled for your project, you'll need to create an API token to be able to authenticate when consuming the API from your .NET project. Navigate to the Credentials page, click on " CREATE CREDENTIALS" and then on " API key".
A modal will appear showing your API key. You will need this API key later, so copy and paste the API key somewhere safe. Next, click on the " Edit API key" link.
On the "ββ Edit API key" page, you can configure the name and restrictions for your API key. This step isn't strictly required, but it is a good practice to follow the principle of least privilege, which means you should only give accounts and keys the level of access necessary to do their job.
In that spirit, find the " API restrictions" field and click on the " Restrict key" radio button.
Click on the newly appeared dropdown and select the " Google Civic Information API" option, and then hit Save.
You're all done setting up your GCP project and API key. Let's go try out the API!
Try out the Civic Information API
Open a new tab in your browser and navigate to the Civic Information API documentation for getting representatives by address.The representativeInfoByAddress
will be the specific method that you'll be using for this project.
You can pass a couple of parameters to the method to filter down the representative information, most importantly the required address
field. This address
field expects a physical mail address, however, it can be a partial address. You can pass in just the ZIP code, the city and state, or a full address including the street and number.
The HTTP response will contain divisions
, offices
, and officials
for the given address:
-
divisions
are the political geographic areas that the address is within. Among these you would find the U.S. congressional districts. -
offices
are the political offices someone can be elected to within the returneddivisions
. -
officials
are the elected officials holding the returnedoffices
.
By default, representativeInfoByAddress
will return all divisions, offices, and officials that represent the given address, all the way from the highest office of the President down to the local offices.
To filter this down, you can pass in the levels
and roles
parameters.
This bot will look for U.S. representatives, so to filter down to only return representatives, you'll need to pass in country
into the levels
parameter because the U.S. representative is a national office, and you'll need to pass in legislatorLowerBody
to the roles
parameter because U.S. Congress has two houses: the Senate is the upper body and the House of Representatives is the lower body.
Click the "Try it now" link at the top of the page and play around with the method.
Set the address
field to 10600 Little Run Farm Ct, Vienna
, the levels
to country
, and roles
to legislatorLowerBody
, and then click EXECUTE.
The response should have a single division, office, and official.
New congressional district maps are going into effect for the November 2022 elections, so the maps and data will change soon!
Now change the address
field to Little Run Farm Ct, Vienna
and click EXECUTE again.
This time no division, office, or official is returned. Why is that?
When you look up this street and overlay the congressional district maps, you can see that the border of district 10 and 11 cuts through this street. In fact, the border follows a creek that runs between house number 10600 and 10602.
That's why the Civic Information API cannot determine the district when you pass in the street without a house number, and as a result, representativeInfoByAddress
returns nothing.
The house.gov representative lookup tool will claim that house number 10600 and 10602 are both in Virginia's 11th congressional district which is incorrect (for the pre-2022 maps which are current at the time of writing this). Luckily, the Civic Information API returns the correct result.
This does not mean you always have to enter a full address, in fact when you put in the ZIP code 20301 without anything else, representativeInfoByAddress
returns the information for the 8th congressional district. That's because the entire ZIP code is located within district 8, so the API doesn't need more information to determine the district.
Now that you're more familiar with the Civic Information API and specifically the representativeInfoByAddress
method, it's time to consume the API from .NET!
Build an SMS bot to look up representatives
This project will be built on ASP.NET Core Minimal APIs. Open a shell and run the following command to create the project and navigate into the project folder:
dotnet new web -o RepresentativeBot
cd RepresentativeBot
Open the project in your preferred editor.
Retrieve Representatives from the Civic Information API
You can consume the Civic Information API using an HTTP client, but Google generates .NET libraries for all of its APIs including this one, which is convenient. You can install the library by adding the Google.Apis.CivicInfo.v2 NuGet packageusing the following command:
dotnet add package Google.Apis.CivicInfo.v2
Create a new file RepresentativeLookupClient.cs and add the following C# code:
using System.Net;
using Google;
using Google.Apis.CivicInfo.v2;
using Google.Apis.CivicInfo.v2.Data;
using Google.Apis.Services;
using static Google.Apis.CivicInfo.v2.RepresentativesResource;
namespace RepresentativeBot;
public class RepresentativeLookupClient
{
private readonly CivicInfoService service;
public RepresentativeLookupClient(string gcpApiKey)
{
service = new CivicInfoService(new BaseClientService.Initializer
{
ApplicationName = "US Representative Lookup",
ApiKey = gcpApiKey
});
}
public async Task<Representative> GetRepresentativeByAddress(string address)
{
var request = new RepresentativeInfoByAddressRequest(service)
{
Address = address,
// Level = Country and Roles = LegislatorLowerBody filters down to U.S. Representatives
Levels = RepresentativeInfoByAddressRequest.LevelsEnum.Country,
Roles = RepresentativeInfoByAddressRequest.RolesEnum.LegislatorLowerBody
};
RepresentativeInfoResponse response;
try
{
response = await request.ExecuteAsync();
}
catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.BadRequest &&
e.Error.Message == "Failed to parse address")
{
throw new FailedToParseAddressException(e);
}
// if the address did not resolve to a specific district,
// for example, no district, or multiple districts
// then response.Offices will be null
if (response.Offices == null)
{
throw new RepresentativeNotFoundException();
}
// only one office, one division, and one official will be returned
// the office of U.S. representative, the congressional district, and the elected official
var office = response.Offices[0];
var division = response.Divisions[office.DivisionId];
var official = response.Officials[0];
return new Representative
{
DistrictName = division.Name,
RepresentativeName = official.Name,
Party = official.Party,
PhotoUrl = official.PhotoUrl
};
}
}
The RepresentativeLookupClient
constructor accepts the GCP API key as the gcpApiKey
parameter, which is then used to create a new CivicInfoService
object that is stored into the service
field.
The GetRepresentativeByAddress
method accepts an address
as a parameter and returns a Task
that will resolve to a Representative
object. In this method, a RepresentativeInfoByAddressRequest
object is created passing in the service
into the constructor and the address
into the Address
property.
The Levels
and Roles
property are hard-coded to Country
and LegislatorLowerBody
to filter down to U.S. representatives and congressional districts.
The API request is made using the request.ExecuteAsync
method, which is surrounded by a try/catch block to handle some of the exceptions that can be thrown. When the API fails to parse the address, a custom exception of type FailedToParseAddressException
is thrown. When no offices are returned, because the API couldn't determine the correct congressional district, or for some other reason, then a custom exception of RepresentativeNotFoundException
is thrown. These two custom exceptions will be caught later to provide user-friendly messages to the end user.
However, if an office is returned, then a new Representative
object is created with the data from the response
.
The Representative
object only has a couple of properties, however, feel free to extend the class and retrieve more information from the response
object. The API retrieves a bunch of information like a physical address, an email address, a phone number, social media links, etc.
For example, here's a tutorial that uses the same API to retrieve contact information of the representatives and uses Twilio SMS and SendGrid to contact them, built on Twilio Studio and Functions.
Create 3 new files named Representative.cs, FailedToParseAddressException.cs, and RepresentativeNotFoundException.cs and then update each file with the contents listed below.
Representative.cs:
namespace RepresentativeBot;
public class Representative
{
public string DistrictName { get; set; }
public string RepresentativeName { get; set; }
public string Party { get; set; }
public string PhotoUrl { get; set; }
}
FailedToParseAddressException.cs:
namespace RepresentativeBot;
public class FailedToParseAddressException : Exception
{
public FailedToParseAddressException(Exception innerException) : base(innerException.Message, innerException)
{
}
}
RepresentativeNotFoundException.cs:
namespace RepresentativeBot;
public class RepresentativeNotFoundException : Exception
{
}
To use the RepresentativeLookupClient
in your future API endpoints, you'll need to add it to ASP.NET Core's Dependency Injection (DI) container. Open your Program.cs file and add the highlighted lines to the existing code:
using RepresentativeBot;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped(provider =>
{
var configuration = provider.GetRequiredService<IConfiguration>();
var gcpApiKey = configuration["GcpApiKey"] ?? throw new Exception("GcpApiKey is not configured.");
return new RepresentativeLookupClient(gcpApiKey);
});
var app = builder.Build();
...
The GCP API key you created earlier needs to be configured using the GcpApiKey
configuration key. Since the API key is a secret, you should avoid hard-coding it or storing it in source control.You can use environment variables or a secure vault service, or for local development, you can use the .NET Secrets Manager also known as user secrets.
Run the following command to initialize user secrets in your project:
dotnet user-secrets init
Next, configure the GCP API key as a user secret:
dotnet user-secrets set GcpApiKey "[YOUR_GCP_API_KEY]"
Replace [YOUR_GCP_API_KEY]
with the API key secret you copied earlier.
The RepresentativeLookupClient
is now ready to be used! Feel free to add some code to quickly test it out, otherwise, move to the next step where you'll integrate the client into your minimal API.
Create your Twilio Webhook
There's a useful library called Twilio.AspNet
which will help you build Twilio webhooks.
Add the Twilio.AspNet.Core NuGet package to install the library:
dotnet add package Twilio.AspNet.Core
Then, open your Program.cs file and update it with the following code:
using RepresentativeBot;
using Twilio.AspNet.Core.MinimalApi;
using Twilio.TwiML;
using Twilio.TwiML.Messaging;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped(provider =>
{
var configuration = provider.GetRequiredService<IConfiguration>();
var gcpApiKey = configuration["GcpApiKey"] ?? throw new Exception("GcpApiKey is not configured.");
return new RepresentativeLookupClient(gcpApiKey);
});
var app = builder.Build();
app.MapPost("/message", async (
HttpRequest request,
HttpResponse response,
IServiceProvider serviceProvider,
ILogger<Program> logger
) =>
{
var messagingResponse = new MessagingResponse();
if (bool.Parse(request.Cookies["HasBeenGreeted"] ?? "False") == false)
{
response.Cookies.Append("HasBeenGreeted", "True");
messagingResponse.Message("Welcome to the U.S. Representative lookup bot. Respond with your address.");
return Results.Extensions.TwiML(messagingResponse);
}
// TODO: remove me
messagingResponse.Message("YOU HAVE BEEN GREETED ALREADY!");
return Results.Extensions.TwiML(messagingResponse);
});
app.Run();
The new /message endpoint accepts a couple of parameters that will be dependency injected.
The MessagingResponse
object will help you construct TwiML to respond to an incoming message.
The first message you will respond with, is an introductory message. To only introduce the bot once, a cookie is used to keep track of whether the sender has been greeted with the introduction.
You can use cookies when responding to Twilio webhooks, however, the cookie will expire after 4 hours and you can only store one cookie. In this tutorial you only need to keep track of one thing using cookies, but if you need to keep track of more state you can use a session cookie instead and store state in session. Learn more about the cookie limitations with Twilio webhooks here.
The messagingResponse.Message
method is used to create a TwiML Message
verb that will respond to the sender. The Results.Extensions.TwiML
method creates a TwiMLResult
that will serialize the messagingResponse
to XML and set the correct content-type header.
If the sender has already been greeted, the bot responds with "YOU HAVE BEEN GREETED ALREADY!".
However, this last message is a placeholder. Replace the placeholder with the following code:
var form = await request.ReadFormAsync().ConfigureAwait(false);
var body = form["Body"][0];
var representativeLookupClient = serviceProvider.GetRequiredService<RepresentativeLookupClient>();
try
{
var representative = await representativeLookupClient.GetRepresentativeByAddress(body);
messagingResponse.Message(
$"Your representative is {representative.RepresentativeName} ({representative.Party})" +
$", representing {representative.DistrictName}."
);
if (representative.PhotoUrl is not null)
{
messagingResponse.Append(new Message().Media(new Uri(representative.PhotoUrl)));
}
}
catch (FailedToParseAddressException)
{
messagingResponse.Message("The address you entered is invalid.");
}
catch (RepresentativeNotFoundException)
{
messagingResponse.Message("Your representative could not be determined. " +
"This may be because there's no representative or multiple representatives for the given location. " +
"Try entering a more specific address.");
}
catch (Exception ex)
{
logger.LogError(ex, "An unexpected error occurred when looking up representative");
messagingResponse.Message("An unexpected error occurred.");
}
Once the sender has been greeted, the bot will assume that whatever message is sent next will be the address to return representative information for. The message that is sent will be stored in the form encoded "Body" parameter.
After getting the body of the message, the bot will request an instance of RepresentativeLookupClient
from the DI container, then the bot invokes the RepresentativeLookupClient.GetRepresentativeByAddress
method passing in the body as a parameter.
If no exception occurs, two messages will be added. One message will describe the district and representative, and the other will return the image of the elected official, if an image is provided.
A TwiML Message
with Media
will send the media as an MMS message, not as an SMS. You can choose to create a single message with the representative information and the image which looks great!
However, I chose to create separate messages because of these reasons:
- An SMS is smaller and will be transmitted to the user faster, giving them the information as quickly as possible.
- MMS uses mobile internet which may not be turned on, or there may be no mobile internet connectivity in rural areas.
- MMS may not be properly configured or disabled.
Your project is ready! Start your project using dotnet run
.
To quickly test out your /message endpoint, you can use the following PowerShell or Bash script in a new shell tab:
PowerShell:
Invoke-WebRequest [YOUR_LOCALHOST_URL]/message `
-Method Post `
-Body @{ Body = 'Hi' } `
-ContentType 'application/x-www-form-urlencoded' `
-SessionVariable SmsBotSession
Invoke-WebRequest [YOUR_LOCALHOST_URL]/message `
-Method Post `
-Body @{ Body = '10602 Little Run Farm Ct, Vienna' } `
-ContentType 'application/x-www-form-urlencoded' `
-WebSession $SmsBotSession
Bash:
curl -X POST [YOUR_LOCALHOST_URL]/message \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "Body=Hi" \
--cookie-jar SmsBotCookieJar
curl -X POST [YOUR_LOCALHOST_URL]/message \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "Body=10602 Little Run Farm Ct, Vienna" \
--cookie SmsBotCookieJar
rm SmsBotCookieJar
Replace [YOUR_LOCALHOST_URL]
with one of the localhost URLs printed out when running your .NET project.
The first web request will be responded to with the following TwiML:
<?xml version="1.0" encoding="utf-8"?>
<Response>
<Message>Welcome to the U.S. representative lookup bot. Respond with your address.</Message>
</Response>
The subsequent request will be responded to with the following TwiML:
<?xml version="1.0" encoding="utf-8"?>
<Response>
<Message>Your representative is Gerald E. "Gerry" Connolly (Democratic Party), representing Virginia's 11th congressional district.</Message>
<Message>
<Media>http://bioguide.congress.gov/bioguide/photo/C/C001078.jpg</Media>
</Message>
</Response>
If everything is working as expected, you can now configure your Twilio Phone Number to use your Minimal API endpoint as the SMS webhook.
Configure your Twilio SMS Webhook
Start your project if it isn't running yet (dotnet run
) and take note of one of the localhost URLs printed out by the application. For Twilio to be able to send HTTP requests to your local web server, the server needs to become publicly accessible. ngrok is a free secure tunneling service that can make your local web servers public.
Run the following ngrok command in a separate shell:
ngrok http [YOUR_ASPNET_URL]
Replace [YOUR_ASPNET_URL]
with the localhost URL from your .NET application. If you're using an HTTPS localhost URL, you'll need to authenticate ngrok.
The ngrok command will display an HTTPS Forwarding URL that makes your local web server public.
Now it's time to update your Twilio Phone Number to send HTTP requests to your /message endpoint via the ngrok Forwarding URL. The URL should look something like https://1cc74f4c9f70.ngrok.io/message.
Go to the Active Phone Numbers section in the Twilio Console and click on your Twilio Phone Number.
This will take you to the configuration for the phone number. Find the Messaging section and under the " A MESSAGE COMES IN" label, set the dropdown to Webhook. In the text field next to it, enter your ngrok forwarding URL with /message appended to it. Select β HTTP POST β in the last dropdown.
Finally, click the Save button at the bottom of the page.
Test your SMS Twilio Bot
To test your bot, you can either use a personal phone to send text messages, or you can use the Twilio Dev Phone to test.
Send a text message to your Twilio Phone Number with anything as the body. You should receive the greeting you configured. Once you've been greeted, reply with any address that you want to retrieve representative and district information for.
Future improvements
This bot is a great start, however, you can improve this solution in a couple of ways:
You can respond to the user with more information about the elected official and how to reach them. And instead of only returning U.S. representatives, you could return elected officials from every level.
Currently, anyone can send HTTP requests to your webhook and pretend to be Twilio. This bot doesn't respond with any sensitive information so the risk is lower, however, it is always a good idea to secure your webhooks using webhook signature validation.
In this tutorial, you manually spun up a secure tunnel using ngrok, but you could also do this programmatically so that a tunnel is created and your Twilio SMS webhook is updated whenever you start your .NET application.
Next steps
If you made it this far, a big shout-out to you! π
In this tutorial, you learned how to respond to incoming text messages with Twilio, but if youβre looking to learn more, you can also initiate text messages yourself using the Twilio SDK, or send emails using Twilio SendGrid.
If you found this useful, let me know and share what you're working on. I can't wait to see what you build!
Image credit: Matthew Geason has kindly allowed us to use his picture of the U.S. Capitol.
Posted on August 4, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 4, 2022