APIs provided by Google Docs include creating new document, getting a document and updating a document. However, the function is limited. For example, people can't copy paragraphs; insert text or update styles need the index of the text in Google docs.
Because there are not many example codes for ASP.NET core with C#, I write this article.
To integrate with Google Docs, there are steps:
- turn on the Google Docs API.
- Prepare the project
- Manipulate docs in Google Docs
Turn on the Google Docs API & Google Drive API
Creating credential can either refer the official website of Google Docs https://developers.google.com/docs/api/quickstart/dotnet or this article https://thecodehubs.com/google-drive-integration-in-asp-net-mvc/.
Generally, the processing of create credential of Google Docs and Google drive are almost the same. Since I need to copy documents, I need to enable the both APIs. Under the same project in "console.developers.google.com" in "Library" tag, search for them and click enable button.
Prepare the project
Open the NuGet Package Manager Console, install NuGet package of the both API.
Install-Package Google.Apis.Docs.v1
Install-Package Google.Apis.Drive.v3
Install-Package Google.Apis.Drive.v2
Manipulate docs in Google docs
I create a tool class for the functions of manipulate docs, and simply use them in the controller without any set up in Program.cs or Startup.cs. The authentication is done also in the tool class.
Create credential & service
using Google.Apis.Auth.OAuth2;
using Google.Apis.Docs.v1;
using Google.Apis.Docs.v1.Data;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using System.Threading;
using Microsoft.AspNetCore.Hosting;
using Google.Apis.Drive.v3;
namespace Talent.Api.Domain.Services
{
public class ProfileService : IProfileService
{
static string[] Scopes = { DocsService.Scope.Documents,DocsService.Scope.Drive };
static string ApplicationName = "Google Docs API Export";
static string TemplateDocId = "1WwKfzg0dSjEnZRaOIchD2TT8RqIORsCBfnGpCil1R0c";
public async Task<bool> ExportProfileToGoogleDocs(TalentProfileViewModel model)
{
try
{
UserCredential credential;
using (var stream = new FileStream(hostingEnvironment.ContentRootPath +
"//credentials.json", FileMode.Open, FileAccess.Read))
{
string credPath =
Environment.GetFolderPath(Environment.SpecialFolder.Personal);
credPath = Path.Combine(credPath,
$"./credentials/{model.Id}/credentials.json");
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
Scopes,
"user",
CancellationToken.None,
new FileDataStore(credPath, true)).Result;
}
// Create Google Docs API service.
var service = new DocsService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
var serviceDrive = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
}
}
}
}
Copy template
Use template to generate a document, since replace the text in template with wanted text can maintain the style of the content. So I don't need to specify every style of a letter.
// copy template
var copyDoc = serviceDrive.Files.Copy(new Google.Apis.Drive.v3.Data.File() { Name =
$"Export Profile for {model.FirstName}" },TemplateDocId).Execute();
var docCopyId = copyDoc.Id;
var doc = service.Documents.Get(docCopyId).Execute();
If you look at the network of the request of "get()", you can see the structure of the document in Google Doc.
Export text into the document
I get some data from the front-end to export to the document. Here I use the tool class to generate the request list.
// export data to google doc
ExportDataToGoogleDocs export = new ExportDataToGoogleDocs();
List<Request> requests = export.GetRequest(model, doc.Body.Content);
BatchUpdateDocumentRequest body = new BatchUpdateDocumentRequest() { Requests = requests };
BatchUpdateDocumentResponse response = service.Documents
.BatchUpdate(body, doc.DocumentId).Execute();
Codes in the tool class
There are codes of insert text, replace text, update text style, create and clear bullet, etc.
public class ExportDataToGoogleDocs
{
public List<Request> GetRequest(DataViewModel model,
IList<StructuralElement> DocContent)
{
try
{
List<Request> requests = new List<Request>();
string text = null;
if (model?.Certifications.Count > 0)
{
int index = GetIndex("{{certification}}", DocContent);
if (index > 0)
{
int length = 0;
for (int i = model.Certifications.Count; i > 0; i--)
{
var cert = model.Certifications[i - 1];
requests.Add(InsertTextRequest(cert.Name + ", "
+ cert.From + ", " + cert.Year + "\n", index));
length = (cert.Name + ", " + cert.From
+ ", " + cert.Year).Length;
requests.Add(UpdateTextStyleRequest(index, length));
length = cert.Name.Length;
requests.Add(UpdateTextStyleRequest(index, length, 11.0,
true));
}
}
}
else
{
text = "Your certification\n";
}
requests.Add(NewReplaceRequest("{{certification}}", text));
text = null;
if (model.Education.Count > 0)
{
int index = GetIndex("{{education}}", DocContent);
if (index > 0)
{
int length = 0;
for (int i = model.Education.Count; i > 0; i--)
{
var education = model.Education[i - 1];
requests.Add(InsertTextRequest(education.Degree + ", "
+ education.YearOfGraduation + ", " +
education.InstituteName
+ ", " + education.Country + "\n", index));
length = (education.Degree + ", " +
education.YearOfGraduation + ", "
+ education.InstituteName + ", " +
education.Country).Length;
requests.Add(UpdateTextStyleRequest(index, length));
length = education.Degree.Length;
requests.Add(UpdateTextStyleRequest(index, length, 11.0,
true));
}
}
}
else { text = "Your education\n"; }
requests.Add(NewReplaceRequest("{{education}}", text));
text = null;
if (model.Experience.Count > 0)
{
int index = GetIndex("{{experience}}", DocContent);
if (index > 0)
{
for (var i = model.Experience.Count; i > 0; i--)
{
var experience = model.Experience[i - 1];
foreach (var desc in
experience.Responsibilities.Split("\n").Reverse())
{
if (desc.Length == 0) continue;
requests.Add(InsertTextRequest(desc + "\n", index));
requests.Add(UpdateTextStyleRequest(index, desc.Length));
requests.Add(AddBulletStyleRequest(index, desc.Length));
}
requests.Add(InsertTextRequest(experience.Position + "\n",
index));
requests.Add(ClearBulletStyleRequest(index));
requests.Add(UpdateTextStyleRequest(index,
experience.Position.Length, 11.0, true));
var info = experience.Company + ", " +
experience.Start.ToShortDateString()
+ " - " + experience.End.ToShortDateString() + "\n";
requests.Add(InsertTextRequest(info, index));
requests.Add(UpdateTextStyleRequest(index, info.Length));
requests.Add(UpdateTextStyleRequest(index,
experience.Company.Length, 11.0, true));
}
}
}
else { text = "Your experience\n"; }
requests.Add(NewReplaceRequest("{{experience}}", text));
text = null;
int skillIndex = GetIndex("{{skills}}", DocContent);
if (skillIndex > 0)
{
if (model.Skills.Count > 0)
{
foreach (var skill in model.Skills.AsEnumerable().Reverse())
{
requests.Add(InsertTextRequest(skill.Name + "\n",
skillIndex));
requests.Add(AddBulletStyleRequest(skillIndex,
skill.Name.Length));
}
}
else { text = "Your skills\n"; }
requests.Add(NewReplaceRequest("{{skills}}", text));
}
text = (model.FirstName.Length > 0 && model.LastName.Length > 0) ?
model.FirstName + " " + model.MiddleName + " " + model.LastName
: "Your Name";
requests.Add(NewReplaceRequest("{{name}}", text));
text = model.Address.City.Length > 0 && model.Address.Country.Length > 0
&& model.Address.Street.Length > 0 ?
model.Address.Number + " " + model.Address.Street + ", " +
model.Address.Suburb + ", "
+ model.Address.City + ", " + model.Address.Country :
"Your address";
requests.Add(NewReplaceRequest("{{address}}", text));
text = model.Phone ?? "021 *** ***";
requests.Add(NewReplaceRequest("{{phone}}", text));
text = model.Email ?? "name@example.com";
requests.Add(NewReplaceRequest("{{email}}", text));
text = model.Summary ?? "Something about you";
requests.Add(NewReplaceRequest("{{summary}}", text));
return requests;
}
catch (Exception)
{
return null;
}
}
private static Request NewReplaceRequest(string preContent,
string replaceContent)
{
Request request = new Request();
request.ReplaceAllText = new ReplaceAllTextRequest()
{
ContainsText = new SubstringMatchCriteria()
{
Text = (preContent),
MatchCase = true
},
ReplaceText = replaceContent
};
return request;
}
private static Request InsertTextRequest(string content, int index)
{
Request request = new Request();
request.InsertText = new InsertTextRequest()
{
Text = content,
Location = new Location() { Index = index }
};
return request;
}
private static Request AddBulletStyleRequest(int startIndex, int length)
{
Request request = new Request();
request.CreateParagraphBullets = new CreateParagraphBulletsRequest
{
Range = new Range { StartIndex = startIndex, EndIndex = startIndex +
length },
BulletPreset = "BULLET_DISC_CIRCLE_SQUARE"
};
return request;
}
private static Request ClearBulletStyleRequest(int startIndex)
{
Request request = new Request();
request.DeleteParagraphBullets = new DeleteParagraphBulletsRequest
{
Range = new Range { StartIndex = startIndex, EndIndex = startIndex + 1 }
};
return request;
}
private static Request UpdateTextStyleRequest(int startIndex, int length,
double fontSize = 11, bool bold = false)
{
Request request = new Request();
request.UpdateTextStyle = new UpdateTextStyleRequest()
{
TextStyle = new TextStyle()
{
FontSize = new Dimension() { Magnitude = fontSize, Unit = "PT" },
Bold = bold
},
Range = new Range() { StartIndex = startIndex, EndIndex = startIndex +
length },
Fields = "fontSize, bold"
};
return request;
}
private static int GetIndex(string stringToFind,
IList<StructuralElement> DocContent)
{
var result = DocContent.Where(a => a.Paragraph != null)
.FirstOrDefault(x => x.Paragraph.Elements.Any(
y => y.TextRun.Content.StartsWith(stringToFind,
StringComparison.CurrentCulture)
));
if (result == null)
{
return 0;
}
return (int)result.StartIndex;
}
}