Attachment summarization using .NET, OpenAI API Assistant and Twilio SendGrid

July 10, 2025
Written by
Eman Hassan
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Attachment summarization using .NET, OpenAI API Assistant and Twilio SendGrid

Introduction

Processing files is essential in many business domains. One of the common needs is summarization of text, and Generative AI has made this an easy and effortless task. In this post, you’ll learn how to build a .NET API that receives attachments and uses OpenAI API assistance to summarize files. You'll then receive an email notification when the summarization is complete using Twilio Sendgrid.

Prerequisites

The first thing you will need to do is to configure the summarization process.

Setting up OpenAI API Assistant

You will use OpenAI API Assistant, which will be configured to process the text files and summarize them.

Login or signup to the assistant’s page. Then click on the Create button, which will open a form to start building the assistant.

Create openai assistant

Fill in assistant details as follows:

  • Name: “File Summarizer”
  • Instructions: “You are an assistant that reads and summarizes the content of uploaded text files. Your job is to extract the main points clearly and concisely for users without losing important context”
  • Model: gpt-4-turbo

Then configure the tools to use File Search to be able to read and summarize the files properly. Set the temperature to a value less than 1 to decrease the amount of randomness and avoid AI hallucinations.

After you are done configuring the assistant, copy and keep its ID. You will need it in the implementation of the application in .NET.

Copy the id

Before you start building the application, you can test the assistant first in the assistant’s playground by clicking on the Playground button as shown below:

open assistant playground

It will then open the playground as shown below. You will be able to attach and summarize your files by clicking the attached icon and choosing File Search. Attach your document. The summarizer supports the filetypes .txt, .pdf or .docx – you can check if the file you have is supported in the OpenAI Supported Files List. Then run the assistant.

Try the assistant

Then it will generate the summary for your text as shown below:

Summary example

You can tweak the summarization instructions or the prompt itself when you attach the file to make it more customized, or provide examples of how the output needs to look. When you reach results that you are satisfied with, then move on to using the assistant’s API in your application.

Before you start using the API you need to generate an API Key. To create the API Key, navigate to API Keys page here, then click on Create New Secret Key.

Add a name to your secret key, then click Create Secret Key. It will then show your generated API key. It is important to copy it and save it in secure storage because you will not be able to see it again for security standards.

Set up SendGrid API Key

You’ll need to create a new SendGrid API Key with restricted access. On the Restricted Access tab, be sure your API Key is set to allow Email Sending access. Give your API key a name, and click Create + View. Keep this key somewhere safe as you will need it in the subsequent steps, and SendGrid will not show it again.

Building the .NET API Application

Open Visual Studio Code and create a new, empty folder. Open the terminal window and run the following commands to create the .NET web API:

dotnet new webapi -n AttachmentSummarizer
cd AttachmentSummarizer

Run the following commands to install the needed packages for Sendgrid, JSON and HTTP Features:

dotnet add package System.Net.Http.Json
dotnet add package SendGrid
dotnet add package Microsoft.AspNetCore.Http.Features dotnet add package Swashbuckle.AspNetCore

Open the file AppSettings.json. Beneath the existing logging settings and between the outer brackets, add the following code to configure OpenAI and SendGrid.

"OpenAI": {
      "ApiKey": "sk-...",
      "AssistantId": "asst_...",
      "OpenAIBaseUrl": "https://api.openai.com/v1"
    },
    "SendGrid": {
      "ApiKey": "your-sendgrid-key",
      "FromEmail": "your-email@example.com"
    },

Replace the OpenAI API key value with the one you obtained in the previous steps, and add the ID of the assistant you created in OpenAI for summarization. Replace the API key for SendGrid with the one you obtained for your account and the “FromEmail” with the email you authorized to use.

Create a Controllers folder in the AttachmentSummarizer directory. Then, create a new file named UploadController.cs. Add the following code to create the endpoint to upload an attachment and receive the attachment’s summary by email:

using Microsoft.AspNetCore.Mvc;
namespace AttachmentSummarizer
{
    [ApiController]
    [Route("api/[controller]")]
    public class UploadController : ControllerBase
    {
        private readonly OpenAIService _openAI;
        private readonly EmailService _email;
        public UploadController(OpenAIService openAI, EmailService email)
        {
            _openAI = openAI;
            _email = email;
        }
        [HttpPost("summarize")]
        public async Task<IActionResult> Summarize([FromForm] IFormFile file, [FromForm] string email)
        {
            if (file == null || file.Length == 0)
                return BadRequest("No file uploaded.");
            var stream = new MemoryStream();
            await file.CopyToAsync(stream);
            stream.Position = 0;
            var summary = await _openAI.SummarizeFileAsync(file.FileName, stream);
            await _email.SendSummaryEmail(email, file.FileName, summary);
            return Ok(new { message = "Summary sent to email." });
        }
    }
}

Some of this code will not be working yet. However, we'll add the necessary services for your application in the next step.

Implement OpenAi assistant API logic

Create a Services folder. Then create a new file called OpenAIService.cs and add the following code to implement the assistant call then send the attachment, and run the summarization request:

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AttachmentSummarizer
{
    public class OpenAIService
    {
        private readonly HttpClient _httpClient;
        private readonly string _apiKey;
        private readonly string _assistantId;
        private readonly string _baseUrl;
        public OpenAIService(IConfiguration config)
        {
            _httpClient = new HttpClient();
            _apiKey = config["OpenAI:ApiKey"];
            _assistantId = config["OpenAI:AssistantId"];
            _baseUrl = config["OpenAI:OpenAIBaseUrl"];
            _httpClient.DefaultRequestHeaders.Authorization =
     new AuthenticationHeaderValue("Bearer", _apiKey);
            _httpClient.DefaultRequestHeaders.Add("OpenAI-Beta", "assistants=v2");
        }
        public async Task<string> SummarizeFileAsync(string fileName, Stream stream)
        {
            // Step 1: Upload file
            var content = new MultipartFormDataContent();
            content.Add(new StreamContent(stream), "file", fileName);
            content.Add(new StringContent("assistants"), "purpose");
            var fileUploadResp = await _httpClient.PostAsync($"{_baseUrl}/files", content);
            var fileResult = JsonSerializer.Deserialize<JsonElement>(await fileUploadResp.Content.ReadAsStringAsync());
            var fileId = fileResult.GetProperty("id").GetString();
            // Step 2: Create a thread
            var threadResp = await _httpClient.PostAsync($"{_baseUrl}/threads", new StringContent("{}", System.Text.Encoding.UTF8, "application/json"));
            var threadResult = JsonSerializer.Deserialize<JsonElement>(await threadResp.Content.ReadAsStringAsync());
            var threadId = threadResult.GetProperty("id").GetString();
            // Step 3: Add message to thread
            var messageData = new
            {
                role = "user",
                content = "Please summarize the attached file.",
            };
            var messageJson = JsonSerializer.Serialize(messageData);
            await _httpClient.PostAsync($"{_baseUrl}/threads/{threadId}/messages", new StringContent(messageJson, System.Text.Encoding.UTF8, "application/json"));
            // Step 4: Run assistant
            var rd = await CreateVectorStoreAndAttachFile(fileId);
            var runData = new
            {
                assistant_id = _assistantId,
                tool_resources = new
                {
                    file_search = new
                    {
                        vector_store_ids = new[] { rd }
                    }
                }
            };
            var runResp = await _httpClient.PostAsync($"{_baseUrl}/threads/{threadId}/runs", new StringContent(JsonSerializer.Serialize(runData), System.Text.Encoding.UTF8, "application/json"));
            var runResult = JsonSerializer.Deserialize<JsonElement>(await runResp.Content.ReadAsStringAsync());
            var runId = runResult.GetProperty("id").GetString();
            // Step 5: Poll until complete
            while (true)
            {
                var runStatusResp = await _httpClient.GetAsync($"{_baseUrl}/threads/{threadId}/runs/{runId}");
                var runStatusJson = JsonSerializer.Deserialize<JsonElement>(await runStatusResp.Content.ReadAsStringAsync());
                var status = runStatusJson.GetProperty("status").GetString();
                if (status == "completed") break;
                await Task.Delay(1000);
            }
            // Step 6: Get messages
            var messageListResp = await _httpClient.GetAsync($"{_baseUrl}/threads/{threadId}/messages");
            var messageListJson = JsonSerializer.Deserialize<JsonElement>(await messageListResp.Content.ReadAsStringAsync());
            var summary = messageListJson
                .GetProperty("data")[0]
                .GetProperty("content")[0]
                .GetProperty("text")
                .GetProperty("value")
                .GetString();
            return summary ?? "No summary found.";
        }
        public async Task<string> CreateVectorStoreAndAttachFile(string fileId)
        {
            var payload = new
            {
                file_ids = new[] { fileId }
            };
            var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
            var response = await _httpClient.PostAsync($"{_baseUrl}/vector_stores", content);
            var json = await response.Content.ReadAsStringAsync();
            var root = JsonDocument.Parse(json).RootElement;
            return root.GetProperty("id").GetString(); // vector_store_id
        }
    }
}

The code above performs the following steps to send and summarize the file:

  1. Upload File to OpenAI
    1. Uploads the user-provided file using purpose=assistants.
    2. Returns a file_id.
  2. Create Vector Store
    1. Creates a vector store and attaches the uploaded file to it.
    2. Returns a vector_store_id.
  3. Wait for Vector Store to Be Ready
    1. Polls the vector store’s status until it is completed (fully indexed and ready for search).
  4. Create a New Thread
    1. Initializes a conversation thread where messages and assistant runs are tracked.
    2. Returns a thread_id.
  5. Add a User Message
    1. Adds a message to the thread with instructions like “Please summarize the uploaded document.”
    2. Ensures the assistant has context for what to do.
  6. Create Assistant Run with File Search
    1. Starts the assistant run, providing both the assistant_id and vector_store_id in tool_resources.file_search.
    2. Enables the assistant to access and search the uploaded file.
  7. Poll Run Until Completed
    1. Periodically checks the run’s status (queued, in_progress, etc.) until it reaches completed.
  8. Retrieve Assistant’s Response
    1. Fetches the final summary message from the thread.
  9. Returns the assistant’s response as a string.

Implement SendGrid email service

Create a new file, EmailService.cs, in your Services folder. Add the following code to implement the email send logic:

using SendGrid;
using SendGrid.Helpers.Mail;
namespace AttachmentSummarizer
{
    public class EmailService
    {
        private readonly string _apiKey;
        private readonly string _from;
        public EmailService(IConfiguration config)
        {
            _apiKey = config["SendGrid:ApiKey"];
            _from = config["SendGrid:FromEmail"];
        }
        public async Task SendSummaryEmail(string toEmail, string filename, string summary)
        {
            var client = new SendGridClient(_apiKey);
            var from = new EmailAddress(_from, "AI Summarizer");
            var to = new EmailAddress(toEmail);
            var subject = $"Summary of {filename}";
            var plainTextContent = summary;
            var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, null);
            await client.SendEmailAsync(msg);
        }
    }
}

Register the services

Open the Program.cs file, and replace the entire code with the following code, adding the services created for OpenAI and SendGrid:

using AttachmentSummarizer;
using Microsoft.AspNetCore.Http.Features;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddScoped<OpenAIService>();
builder.Services.AddScoped<EmailService>();
builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = 50 * 1024 * 1024; // 50MB
});
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Testing the application

Run the following command to run the application:

dotnet run

Copy the URL of your localhost running endpoint and append to it “/api/upload/summarize” to call the endpoint you are testing. Open Postman and use a POST request with the body configured as “form-data” with the first item name “file” of type File then attach the file you want to summarize. Then add another item with the name “email” and add the email you want to receive the summary on.

test the app using postman

You should then receive a summary to your email that will look like the sample below:

sample summary email
Running the API multiple times will take from your credit balance in OpenAI, so make sure to keep an eye on the usage amount. For example, a couple of runs to the AI assistant for the above summary cost around $0.06. Check more on their pricing here.

Troubleshooting your application

If you receive any errors or you don’t receive the email, make sure you check the following:

  • Make sure that your sender email is added as a verified email in SendGrid.
  • Check your spam folder
  • Make sure the receiver email is correct
  • Make sure you have valid API keys for both SendGrid and OpenAI

Wrapping up

Summarization is one of the features LLMs excel at, enabling users to quickly extract meaningful insights from large volumes of text with minimal effort. By integrating OpenAI’s Assistants API into your .NET application, you can automate this process across various file formats, delivering concise, context-aware summaries directly to users. Combined with tools like Twilio SendGrid, the solution becomes not just intelligent, but also actionable, empowering users with relevant information right when they need it.

Eman Hassan, a dedicated software engineer and a technical author, prioritizes innovation, cutting-edge technologies, leadership, and mentorship. She finds joy in both learning and sharing expertise with the community. Explore more courses, code labs and tutorials authored by Eman here https://app.pluralsight.com/profile/author/eman-m