Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .agents/build-release-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ Procedure-level detail for humans is in [`RELEASING.md`](../RELEASING.md); this
`netstandard2.0` is what covers .NET Framework / older runtimes — there are **no** `net481`/`net462`
TFMs anymore (dropped in 2.0, along with `net6.0`).
- **DI extension:** mirrors the same three TFMs.
- **Templates package `GoogleMapsApi.Templates`** (`dotnet new` template, ships the `googlemaps-webapi`
template): a `PackageType=Template` package — no build output (`IncludeBuildOutput=false`,
`EnableDefaultCompileItems=false`), so its single `netstandard2.0` TFM is metadata-only. Its
`templates/` tree packs under `content/`. A `BeforeTargets="_GetPackageFiles"` target stamps the
resolved MinVer version into the packed `template.json`, so generated projects reference the
matching `GoogleMapsApi` version.
- **Tests:** `net8.0; net10.0`. **Samples:** `net10.0; net8.0`.
- `LangVersion=latest`, `Nullable=enable`, `GenerateDocumentationFile=true` (CS1591 suppressed).

Expand All @@ -18,7 +24,9 @@ Procedure-level detail for humans is in [`RELEASING.md`](../RELEASING.md); this

Versions come from **git tags** via **MinVer** (`<MinVerTagPrefix>v</MinVerTagPrefix>`). No version is
written in a csproj. `GeneratePackageOnBuild=true`, SourceLink + symbol packages (`.snupkg`) are on.
Both packages are versioned in **lockstep** from the same `v*` tag.
All three packages (`GoogleMapsApi`, `…Extensions.DependencyInjection`, `…Templates`) are versioned in
**lockstep** from the same `v*` tag. Adding a packable project to `GoogleMapsApi.sln` is enough for the
publish workflow to pack and push it — it globs `*.nupkg`, no per-package list.

## Release flow (`release.sh`)

Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ one file relevant to your task.

A strongly-typed .NET wrapper for the Google Maps Web Services APIs (Geocoding, Directions/Routes,
Distance Matrix, Elevation, Time Zone, Places (New), Address Validation, Static Maps). Shipped on
NuGet as `GoogleMapsApi` (+ a sibling `GoogleMapsApi.Extensions.DependencyInjection`).
NuGet as `GoogleMapsApi` (+ siblings `GoogleMapsApi.Extensions.DependencyInjection` and the
`dotnet new` template pack `GoogleMapsApi.Templates`).

## Architecture in 30 seconds

Expand All @@ -21,7 +22,8 @@ NuGet as `GoogleMapsApi` (+ a sibling `GoogleMapsApi.Extensions.DependencyInject
mismatches a compile error.
- **Observability is built in:** one OpenTelemetry span per call from `ActivitySource "GoogleMapsApi"`,
with the API key/signature redacted from the traced URL.
- **Two packages, lockstep-versioned:** core `GoogleMapsApi` and the optional DI extension.
- **Three packages, lockstep-versioned:** core `GoogleMapsApi`, the optional DI extension, and the
`dotnet new` template pack `GoogleMapsApi.Templates`.

Flow: `GoogleMapsClient` → `IEngineFacade` → `HttpClientEngineFacade` (internal) → `MapsAPIGenericEngine` (internal abstract; static-method HTTP+JSON engine).

Expand Down
4 changes: 4 additions & 0 deletions GoogleMapsApi.Templates/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The files under templates/ are template SOURCE and must be committed verbatim so they ship in
# the NuGet package (CI builds from a clean git checkout). The repo-root .gitignore rule
# `**/appsettings.json` would otherwise exclude the template's own appsettings.json — re-include it.
!templates/**/appsettings.json
67 changes: 67 additions & 0 deletions GoogleMapsApi.Templates/GoogleMapsApi.Templates.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<MinVerTagPrefix>v</MinVerTagPrefix>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageType>Template</PackageType>
<PackageId>GoogleMapsApi.Templates</PackageId>
<Title>Google Maps API project templates</Title>
<Authors>Maxim Novak</Authors>
<PackageLicenseExpression>BSD-2-Clause</PackageLicenseExpression>
<Description>dotnet new templates for getting started with GoogleMapsApi. Includes "googlemaps-webapi" — a minimal ASP.NET Core Web API wired to IGoogleMapsClient.</Description>
<Copyright>Copyright © 2010-$([System.DateTime]::Now.Year)</Copyright>
<PackageTags>Google;Maps;API;dotnet-new;Template;Scaffold;AspNetCore</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/maximn/google-maps</RepositoryUrl>
<PackageProjectUrl>https://github.com/maximn/google-maps</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>

<IncludeBuildOutput>false</IncludeBuildOutput>
<ContentTargetFolders>content</ContentTargetFolders>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<NoWarn>$(NoWarn);NU5128;CS1591</NoWarn>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
<NoDefaultExcludes>true</NoDefaultExcludes>
</PropertyGroup>

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

<ItemGroup>
<Content Include="templates\**\*" Exclude="templates\**\bin\**;templates\**\obj\**" />
<Compile Remove="**\*" />
<None Include="..\README.md" Pack="true" PackagePath="\" />
<None Include="..\LICENSE.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="MinVer" Version="7.0.0" PrivateAssets="all" />
</ItemGroup>

<!--
Stamp the just-resolved package version into the packed template.json so every scaffolded
project references the exact GoogleMapsApi version it was generated from. MinVer sets
$(PackageVersion) during pack, before this target runs.
-->
<Target Name="StampTemplateVersion" BeforeTargets="_GetPackageFiles" DependsOnTargets="MinVer">
<PropertyGroup>
<_TemplateConfig>$(IntermediateOutputPath)template.json</_TemplateConfig>
</PropertyGroup>
<Message Importance="high" Text="Stamping template.json with GoogleMapsApi version $(PackageVersion)" />
<ReadLinesFromFile File="templates\googlemaps-webapi\.template.config\template.json">
<Output TaskParameter="Lines" ItemName="_TemplateJsonLines" />
</ReadLinesFromFile>
<WriteLinesToFile File="$(_TemplateConfig)"
Lines="@(_TemplateJsonLines->Replace('PACKAGE_VERSION_TOKEN', '$(PackageVersion)'))"
Overwrite="true" />
<ItemGroup>
<Content Remove="templates\googlemaps-webapi\.template.config\template.json" />
<Content Include="$(_TemplateConfig)"
Pack="true"
PackagePath="content\templates\googlemaps-webapi\.template.config\template.json" />
</ItemGroup>
</Target>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "http://json.schemastore.org/dotnetcli.host",
"symbolInfo": {
"apikey": {
"longName": "apikey",
"shortName": ""
},
"Framework": {
"longName": "framework",
"shortName": "f"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "Maxim Novak",
"classifications": [ "Web", "WebAPI", "Google Maps" ],
"identity": "GoogleMapsApi.WebApi.CSharp",
"name": "Google Maps Web API",
"description": "A minimal ASP.NET Core Web API wired to GoogleMapsApi with /geocode and /directions endpoints.",
"shortName": "googlemaps-webapi",
"defaultName": "GoogleMapsWebApi",
"sourceName": "GoogleMapsWebApi",
"tags": {
"language": "C#",
"type": "project"
},
"preferNameDirectory": true,
"sources": [
{
"rename": {
"gitignore": ".gitignore"
}
}
],
"symbols": {
"apikey": {
"type": "parameter",
"datatype": "string",
"defaultValue": "",
"replaces": "GOOGLE_API_KEY_PLACEHOLDER",
"description": "Google Maps API key written into appsettings.Development.json (kept out of source control)."
},
"Framework": {
"type": "parameter",
"datatype": "choice",
"choices": [
{ "choice": "net10.0", "description": ".NET 10.0" },
{ "choice": "net8.0", "description": ".NET 8.0" }
],
"defaultValue": "net10.0",
"replaces": "FRAMEWORK_TOKEN",
"description": "The target framework for the generated project."
},
"GoogleMapsApiVersion": {
"type": "generated",
"generator": "constant",
"parameters": {
"value": "PACKAGE_VERSION_TOKEN"
},
"replaces": "GOOGLE_MAPS_API_VERSION"
}
},
"primaryOutputs": [
{ "path": "GoogleMapsWebApi.csproj" }
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>FRAMEWORK_TOKEN</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="GoogleMapsApi.Extensions.DependencyInjection" Version="GOOGLE_MAPS_API_VERSION" />
</ItemGroup>

</Project>
94 changes: 94 additions & 0 deletions GoogleMapsApi.Templates/templates/googlemaps-webapi/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using GoogleMapsApi;
using GoogleMapsApi.Entities.Directions.Request;
using GoogleMapsApi.Entities.Geocoding.Request;
using GoogleMapsApi.Entities.Geocoding.Response;

var builder = WebApplication.CreateBuilder(args);

string? apiKey = builder.Configuration["GoogleApiKey"]
?? Environment.GetEnvironmentVariable("GOOGLE_API_KEY");

// Register IGoogleMapsClient (IHttpClientFactory-backed) and the ambient API key in one call.
builder.Services.AddGoogleMaps(options => options.ApiKey = apiKey);

var app = builder.Build();

app.MapGet("/geocode", async (string address, IGoogleMapsClient maps) =>
{
if (string.IsNullOrWhiteSpace(apiKey))
{
return Results.Problem(
"Set your key in appsettings.Development.json (GoogleApiKey) or the GOOGLE_API_KEY env var.",
statusCode: StatusCodes.Status500InternalServerError);
}

if (string.IsNullOrWhiteSpace(address))
{
return Results.BadRequest(new { error = "address is required" });
}

// The ambient ApiKey from the options above is auto-filled into the request.
var response = await maps.Geocode.QueryAsync(new GeocodingRequest { Address = address });

if (response.Status != Status.OK || response.Results is null)
{
return Results.Problem(
$"Geocoding failed: {response.Status}",
statusCode: StatusCodes.Status502BadGateway);
}

return Results.Ok(response.Results.Select(result => new
{
formattedAddress = result.FormattedAddress,
latitude = result.Geometry.Location.Latitude,
longitude = result.Geometry.Location.Longitude,
placeId = result.PlaceId,
types = result.Types,
}));
});

app.MapGet("/directions", async (string origin, string destination, IGoogleMapsClient maps) =>
{
if (string.IsNullOrWhiteSpace(apiKey))
{
return Results.Problem(
"Set your key in appsettings.Development.json (GoogleApiKey) or the GOOGLE_API_KEY env var.",
statusCode: StatusCodes.Status500InternalServerError);
}

if (string.IsNullOrWhiteSpace(origin) || string.IsNullOrWhiteSpace(destination))
{
return Results.BadRequest(new { error = "origin and destination are required" });
}

var request = new DirectionsRequest
{
Origin = origin,
Destination = destination,
};

var response = await maps.Directions.QueryAsync(request);

if (response.Status != GoogleMapsApi.Entities.Directions.Response.DirectionsStatusCodes.OK
|| response.Routes is null)
{
return Results.Problem(
$"Directions failed: {response.Status} {response.ErrorMessage}",
statusCode: StatusCodes.Status502BadGateway);
}

var route = response.Routes.First();
var leg = route.Legs?.FirstOrDefault();

return Results.Ok(new
{
summary = route.Summary,
startAddress = leg?.StartAddress,
endAddress = leg?.EndAddress,
distance = leg?.Distance?.Text,
duration = leg?.Duration?.Text,
copyrights = route.Copyrights,
});
});

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5080",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"GoogleApiKey": "GOOGLE_API_KEY_PLACEHOLDER"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
5 changes: 5 additions & 0 deletions GoogleMapsApi.Templates/templates/googlemaps-webapi/gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bin/
obj/

# Keep your API key out of source control.
appsettings.Development.json
18 changes: 18 additions & 0 deletions GoogleMapsApi.sln
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoogleMapsApi.Benchmarks", "benchmarks\GoogleMapsApi.Benchmarks\GoogleMapsApi.Benchmarks.csproj", "{6429978B-C1E9-415C-B289-DFBF9451393D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoogleMapsApi.Templates", "GoogleMapsApi.Templates\GoogleMapsApi.Templates.csproj", "{F095D702-59A0-4E5D-9E04-B90F1FF1D708}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -190,6 +192,22 @@ Global
{6429978B-C1E9-415C-B289-DFBF9451393D}.Release|x86.Build.0 = Release|Any CPU
{6429978B-C1E9-415C-B289-DFBF9451393D}.Release|x64.ActiveCfg = Release|Any CPU
{6429978B-C1E9-415C-B289-DFBF9451393D}.Release|x64.Build.0 = Release|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Debug|x86.ActiveCfg = Debug|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Debug|x86.Build.0 = Debug|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Debug|x64.ActiveCfg = Debug|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Debug|x64.Build.0 = Debug|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Release|Any CPU.Build.0 = Release|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Release|x86.ActiveCfg = Release|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Release|x86.Build.0 = Release|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Release|x64.ActiveCfg = Release|Any CPU
{F095D702-59A0-4E5D-9E04-B90F1FF1D708}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ dotnet add package GoogleMapsApi

Looking for runnable examples? See [`samples/`](samples/) — console, ASP.NET Core minimal API, and Blazor Server.

## Scaffold a project in 10 seconds

Spin up a working ASP.NET Core Web API (with `/geocode` and `/directions` endpoints) using the `dotnet new` template:

```
dotnet new install GoogleMapsApi.Templates
dotnet new googlemaps-webapi -o MyMapsApi --apikey YOUR_API_KEY
cd MyMapsApi && dotnet run
```

The key is written to `appsettings.Development.json` (gitignored), and the generated project references the matching `GoogleMapsApi` version. Pass `-f net8.0` to target .NET 8.

# Quickstart

## API Key Configuration
Expand Down
Loading