Compare commits

20 Commits

Author SHA1 Message Date
704d6b24dc Don't reference to EF Core as AOT does not yet support it 2024-01-13 13:44:20 -08:00
81d01f28a3 Add initial work to get minimal API's going 2024-01-13 13:30:28 -08:00
bdd247e6e0 Inital work on breaking out the core logic out of the MVC API Web App in preparation to migrating to minimal API's in a seperate project 2024-01-13 12:42:46 -08:00
52708bd2c2 Add self contained runtime deployment 2024-01-13 12:15:55 -08:00
9c5fbc314f Merge branch 'master' of ssh://git.startmywifi.com:2251/AS1024/GeoFeed 2024-01-08 23:21:17 -08:00
1f3778095c Make the code cleaner to read 2024-01-08 23:18:26 -08:00
2d5c318689 Update license text 2024-01-08 14:59:18 -08:00
dd643ab8ab Update the readme file 2024-01-08 10:48:48 -08:00
2d68115f25 Complete the implementation of returning cached data from the local disk cache 2024-01-08 10:41:25 -08:00
020db780a6 Add example connection string 2024-01-08 10:23:02 -08:00
5fec5f4f92 Forward all cancellation tokens and run the local cache first 2024-01-08 10:22:51 -08:00
c43b218974 Add necessary tools 2024-01-08 10:08:46 -08:00
c18e486750 Add initial DB Migrations 2024-01-08 10:08:36 -08:00
9974bb5f0c Create various scopes - forgot that EF Core DBContexts don't work in a singleton service 2024-01-08 10:08:28 -08:00
37841db663 Inject the necessary services into the DI container 2024-01-08 09:57:51 -08:00
e974635cfb Break it out to individual files and initial cache service 2024-01-08 09:57:37 -08:00
2fa8f32f44 Initial work on caching without calling dependencies 2024-01-08 09:18:35 -08:00
6e9d58c6e3 Initial skeleton work of periodic on disk caching 2024-01-08 00:13:08 -08:00
ade2861395 Merge branch 'master' of ssh://git.startmywifi.com:2251/AS1024/GeoFeed 2024-01-07 14:58:49 -08:00
4ea9f2ca7f Update readme and license text 2024-01-07 11:11:20 -08:00
29 changed files with 639 additions and 59 deletions

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AS1024.GeoFeed.Core\AS1024.GeoFeed.Core.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Models\AS1024.GeoFeed.Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
using Microsoft.EntityFrameworkCore;
namespace AS1024.GeoFeed.Core.GeoFeedLocalCache
{
public class GeoFeedCacheDbContext : DbContext
{
public GeoFeedCacheDbContext(DbContextOptions options)
: base(options)
{
}
public virtual DbSet<GeoFeedCacheEntry> GeoFeedCacheEntries { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using AS1024.GeoFeed.Models;
using System.ComponentModel.DataAnnotations;
namespace AS1024.GeoFeed.Core.GeoFeedLocalCache
{
public class GeoFeedCacheEntry : IPGeoFeed
{
[Key]
public int Id { get; set; }
}
}

View File

@@ -0,0 +1,93 @@
using AS1024.GeoFeed.Core.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;
namespace AS1024.GeoFeed.Core.GeoFeedLocalCache
{
public class GeoFeedCacheService : IHostedService
{
private readonly ILogger<GeoFeedCacheService> logger;
private readonly IGeoFeedProvider feedProvider;
private readonly IHost host;
public GeoFeedCacheService(ILogger<GeoFeedCacheService> logger,
IGeoFeedProvider feedProvider,
IHost host)
{
this.logger = logger;
this.feedProvider = feedProvider;
this.host = host;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_ = StartPerioidicSync(cancellationToken);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public async Task<bool> StartPerioidicSync(CancellationToken Token)
{
await DBContextMigrate();
List<GeoFeedCacheEntry> geoFeedCacheEntry = [];
while (!Token.IsCancellationRequested)
{
logger.LogInformation("Running on disk fallback cache process...");
try
{
using var scope = host.Services.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<GeoFeedCacheDbContext>();
var results = await feedProvider.GetGeoFeedData();
results.ForEach(x =>
{
geoFeedCacheEntry.Add(new()
{
Prefix = x.Prefix,
GeolocCity = x.GeolocCity,
GeolocCountry = x.GeolocCountry,
GeolocHasLocation = x.GeolocHasLocation,
GeolocPostalCode = x.GeolocPostalCode,
GeolocRegion = x.GeolocRegion
});
});
if (dbContext.GeoFeedCacheEntries.Any())
{
dbContext.GeoFeedCacheEntries.RemoveRange(dbContext.GeoFeedCacheEntries.ToArray());
}
await dbContext.AddRangeAsync(geoFeedCacheEntry, Token);
await dbContext.SaveChangesAsync(Token);
}
catch (Exception ex)
{
logger.LogWarning("On disk cache failed to run. Waiting on 30 minutes before retry...");
}
await Task.Delay(TimeSpan.FromMinutes(30));
}
return false;
}
private async Task DBContextMigrate()
{
using IServiceScope scope = host.Services.CreateScope();
using GeoFeedCacheDbContext? dbContext =
scope.ServiceProvider.GetService<GeoFeedCacheDbContext>();
#pragma warning disable CS8602 // Dereference of a possibly null reference.
if (dbContext.Database.GetPendingMigrations().Any()) {
await dbContext.Database.MigrateAsync();
}
#pragma warning restore CS8602 // Dereference of a possibly null reference.
}
}
}

View File

@@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace AS1024.GeoFeed.Core.GeoFeedLocalCache
{
public class GeoFeedDesignTimeMigration : IDesignTimeDbContextFactory<GeoFeedCacheDbContext>
{
public GeoFeedCacheDbContext CreateDbContext(string[] args)
{
var builder = new DbContextOptionsBuilder<GeoFeedCacheDbContext>();
builder.UseSqlite("Data Source=migratedb.db");
return new GeoFeedCacheDbContext(builder.Options);
}
}
}

View File

@@ -1,21 +1,22 @@
 using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Interfaces;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace AS1024.GeoFeed.GeoFeedBuilder namespace AS1024.GeoFeed.Core.GeoFeedPreloader
{ {
public class PreLoadGeoFeed : IHostedService public class PreLoadGeoFeed : IHostedService
{ {
private readonly ILogger<PreLoadGeoFeed> logger; private readonly ILogger<PreLoadGeoFeed> logger;
private readonly IGeoFeedProvider provider; private readonly IGeoFeedProvider provider;
private readonly IMemoryCache memoryCache; private readonly IMemoryCache memoryCache;
private readonly IWebHostEnvironment environment; private readonly IHostEnvironment environment;
private const string GeoFeedCacheKey = "GeoFeedData"; private const string GeoFeedCacheKey = "GeoFeedData";
public PreLoadGeoFeed(ILogger<PreLoadGeoFeed> logger, public PreLoadGeoFeed(ILogger<PreLoadGeoFeed> logger,
IGeoFeedProvider provider, IGeoFeedProvider provider,
IMemoryCache memoryCache, IMemoryCache memoryCache,
IWebHostEnvironment environment) IHostEnvironment environment)
{ {
this.logger = logger; this.logger = logger;
this.provider = provider; this.provider = provider;
@@ -29,7 +30,8 @@ namespace AS1024.GeoFeed.GeoFeedBuilder
{ {
if (environment.IsProduction()) if (environment.IsProduction())
await StartPreLoad(); await StartPreLoad();
} catch (Exception ex) }
catch (Exception ex)
{ {
logger.LogWarning($"Failed to preload, exception settings below:\n{ex}"); logger.LogWarning($"Failed to preload, exception settings below:\n{ex}");
} }

View File

@@ -0,0 +1,54 @@
// <auto-generated />
using System;
using AS1024.GeoFeed.Core.GeoFeedLocalCache;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace AS1024.GeoFeed.Core.Migrations
{
[DbContext(typeof(GeoFeedCacheDbContext))]
[Migration("20240113204143_InitialMigration")]
partial class InitialMigration
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("AS1024.GeoFeed.Core.GeoFeedLocalCache.GeoFeedCacheEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("GeolocCity")
.HasColumnType("TEXT");
b.Property<string>("GeolocCountry")
.HasColumnType("TEXT");
b.Property<bool?>("GeolocHasLocation")
.HasColumnType("INTEGER");
b.Property<string>("GeolocPostalCode")
.HasColumnType("TEXT");
b.Property<string>("GeolocRegion")
.HasColumnType("TEXT");
b.Property<string>("Prefix")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("GeoFeedCacheEntries");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AS1024.GeoFeed.Core.Migrations
{
/// <inheritdoc />
public partial class InitialMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "GeoFeedCacheEntries",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GeolocCity = table.Column<string>(type: "TEXT", nullable: true),
GeolocCountry = table.Column<string>(type: "TEXT", nullable: true),
GeolocHasLocation = table.Column<bool>(type: "INTEGER", nullable: true),
GeolocRegion = table.Column<string>(type: "TEXT", nullable: true),
GeolocPostalCode = table.Column<string>(type: "TEXT", nullable: true),
Prefix = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_GeoFeedCacheEntries", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "GeoFeedCacheEntries");
}
}
}

View File

@@ -0,0 +1,51 @@
// <auto-generated />
using System;
using AS1024.GeoFeed.Core.GeoFeedLocalCache;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace AS1024.GeoFeed.Core.Migrations
{
[DbContext(typeof(GeoFeedCacheDbContext))]
partial class GeoFeedCacheDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("AS1024.GeoFeed.Core.GeoFeedLocalCache.GeoFeedCacheEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("GeolocCity")
.HasColumnType("TEXT");
b.Property<string>("GeolocCountry")
.HasColumnType("TEXT");
b.Property<bool?>("GeolocHasLocation")
.HasColumnType("INTEGER");
b.Property<string>("GeolocPostalCode")
.HasColumnType("TEXT");
b.Property<string>("GeolocRegion")
.HasColumnType("TEXT");
b.Property<string>("Prefix")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("GeoFeedCacheEntries");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AS1024.GeoFeed.Models\AS1024.GeoFeed.Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,11 +1,13 @@
using AS1024.GeoFeed.Interfaces; using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Models; using AS1024.GeoFeed.Models;
using System.Text.Json; using System.Text.Json;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Sockets; using System.Net.Sockets;
using System.Web; using System.Web;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace AS1024.GeoFeed.GeoFeedBuilder namespace AS1024.GeoFeed.Core.GeoFeedProviders
{ {
public class NetBoxGeoFeedProvider : IGeoFeedProvider public class NetBoxGeoFeedProvider : IGeoFeedProvider
{ {
@@ -31,7 +33,7 @@ namespace AS1024.GeoFeed.GeoFeedBuilder
public async Task<List<IPGeoFeed>> GetGeoFeedData() public async Task<List<IPGeoFeed>> GetGeoFeedData()
{ {
List<IPGeoFeed> geoFeed = new List<IPGeoFeed>(); List<IPGeoFeed> geoFeed = [];
using HttpClient client = httpClientFactory.CreateClient(); using HttpClient client = httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", configuration["APIKey"]); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", configuration["APIKey"]);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
@@ -46,45 +48,47 @@ namespace AS1024.GeoFeed.GeoFeedBuilder
logger.LogDebug($"Making request to {uri}..."); logger.LogDebug($"Making request to {uri}...");
using HttpResponseMessage result = await client.GetAsync(uri); using HttpResponseMessage result = await client.GetAsync(uri);
if (result.IsSuccessStatusCode) if (!result.IsSuccessStatusCode)
{ {
string stringResult = await result.Content.ReadAsStringAsync(); break;
jsonData = JsonSerializer.Deserialize<NetboxData>(stringResult, new JsonSerializerOptions { }
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower string stringResult = await result.Content.ReadAsStringAsync();
}); jsonData = JsonSerializer.Deserialize<NetboxData>(stringResult, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
if (jsonData?.Results == null || jsonData.Results.Count == 0) if (jsonData?.Results == null || jsonData.Results.Count == 0)
{ {
break; break;
} }
foreach (Result data in jsonData.Results) foreach (Result data in jsonData.Results)
{
try
{ {
try geoFeed.Add(new IPGeoFeed
{ {
geoFeed.Add(new IPGeoFeed Prefix = data.Prefix,
{ GeolocCity = data.CustomFields.GeolocCity,
Prefix = data.Prefix, GeolocRegion = data.CustomFields.GeolocRegion,
GeolocCity = data.CustomFields.GeolocCity, GeolocCountry = data.CustomFields.GeolocCountry,
GeolocRegion = data.CustomFields.GeolocRegion, GeolocHasLocation = data.CustomFields.GeolocHasLocation,
GeolocCountry = data.CustomFields.GeolocCountry, GeolocPostalCode = data.CustomFields.GeolocPostalCode
GeolocHasLocation = data.CustomFields.GeolocHasLocation, });
GeolocPostalCode = data.CustomFields.GeolocPostalCode
});
}
catch (Exception ex)
{
logger.LogError(ex.ToString());
}
} }
catch (Exception ex)
if (!string.IsNullOrEmpty(jsonData.Next))
{ {
uri = new Uri(jsonData.Next); logger.LogError(ex.ToString());
continue;
} }
} }
if (!string.IsNullOrEmpty(jsonData.Next))
{
uri = new Uri(jsonData.Next);
continue;
}
break; break;
} }
} }

View File

@@ -1,6 +1,6 @@
using AS1024.GeoFeed.Models; using AS1024.GeoFeed.Models;
namespace AS1024.GeoFeed.Interfaces namespace AS1024.GeoFeed.Core.Interfaces
{ {
public interface IGeoFeedProvider public interface IGeoFeedProvider
{ {

View File

@@ -1,7 +1,7 @@
using AS1024.GeoFeed.Models; using AS1024.GeoFeed.Models;
using System.Text; using System.Text;
namespace AS1024.GeoFeed.GeoFeedBuilder namespace AS1024.GeoFeed.Core.Tools
{ {
public static class GeoFeedTools public static class GeoFeedTools
{ {

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<PublishAot>true</PublishAot>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,29 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
# Install clang/zlib1g-dev dependencies for publishing to native
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
clang zlib1g-dev
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["AS1024.GeoFeed.MinimalAPI/AS1024.GeoFeed.MinimalAPI.csproj", "AS1024.GeoFeed.MinimalAPI/"]
RUN dotnet restore "./AS1024.GeoFeed.MinimalAPI/./AS1024.GeoFeed.MinimalAPI.csproj"
COPY . .
WORKDIR "/src/AS1024.GeoFeed.MinimalAPI"
RUN dotnet build "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0 AS final
WORKDIR /app
EXPOSE 8080
COPY --from=publish /app/publish .
ENTRYPOINT ["./AS1024.GeoFeed.MinimalAPI"]

View File

@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace AS1024.GeoFeed.MinimalAPI
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
var geoFeed = app.MapGroup("/geofeed.csv");
app.Run();
}
}
}

View File

@@ -0,0 +1,24 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "todos",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5127"
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/todos",
"environmentVariables": {
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true
}
},
"$schema": "http://json.schemastore.org/launchsettings.json"
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -3,7 +3,18 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.8.34330.188 VisualStudioVersion = 17.8.34330.188
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AS1024.GeoFeed", "AS1024.GeoFeed\AS1024.GeoFeed.csproj", "{6292097C-7F35-45BB-B2B0-1918DF49FE7D}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AS1024.GeoFeed", "AS1024.GeoFeed\AS1024.GeoFeed.csproj", "{6292097C-7F35-45BB-B2B0-1918DF49FE7D}"
ProjectSection(ProjectDependencies) = postProject
{8BA82A53-29E7-44A2-91BA-57C15BB4B0F5} = {8BA82A53-29E7-44A2-91BA-57C15BB4B0F5}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AS1024.GeoFeed.Models", "AS1024.GeoFeed.Models\AS1024.GeoFeed.Models.csproj", "{8BA82A53-29E7-44A2-91BA-57C15BB4B0F5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AS1024.GeoFeed.Core", "AS1024.GeoFeed.Core\AS1024.GeoFeed.Core.csproj", "{EE6D6C87-29C4-42A1-8251-7D2BF20F1797}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AS1024.GeoFeed.MinimalAPI", "AS1024.GeoFeed.MinimalAPI\AS1024.GeoFeed.MinimalAPI.csproj", "{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AS1024.GeoFeed.Core.GeoFeedLocalCache", "AS1024.GeoFeed.Core.GeoFeedLocalCache\AS1024.GeoFeed.Core.GeoFeedLocalCache.csproj", "{C474F55D-AE8E-4DF3-A241-FB017551C74A}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -15,6 +26,22 @@ Global
{6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Debug|Any CPU.Build.0 = Debug|Any CPU {6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Release|Any CPU.ActiveCfg = Release|Any CPU {6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Release|Any CPU.Build.0 = Release|Any CPU {6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Release|Any CPU.Build.0 = Release|Any CPU
{8BA82A53-29E7-44A2-91BA-57C15BB4B0F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8BA82A53-29E7-44A2-91BA-57C15BB4B0F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BA82A53-29E7-44A2-91BA-57C15BB4B0F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BA82A53-29E7-44A2-91BA-57C15BB4B0F5}.Release|Any CPU.Build.0 = Release|Any CPU
{EE6D6C87-29C4-42A1-8251-7D2BF20F1797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE6D6C87-29C4-42A1-8251-7D2BF20F1797}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE6D6C87-29C4-42A1-8251-7D2BF20F1797}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE6D6C87-29C4-42A1-8251-7D2BF20F1797}.Release|Any CPU.Build.0 = Release|Any CPU
{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}.Release|Any CPU.Build.0 = Release|Any CPU
{C474F55D-AE8E-4DF3-A241-FB017551C74A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C474F55D-AE8E-4DF3-A241-FB017551C74A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C474F55D-AE8E-4DF3-A241-FB017551C74A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C474F55D-AE8E-4DF3-A241-FB017551C74A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -9,7 +9,22 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AS1024.GeoFeed.Core.GeoFeedLocalCache\AS1024.GeoFeed.Core.GeoFeedLocalCache.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Core\AS1024.GeoFeed.Core.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Models\AS1024.GeoFeed.Models.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,9 +1,10 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using AS1024.GeoFeed.Interfaces; using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.GeoFeedBuilder;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using AS1024.GeoFeed.Models; using AS1024.GeoFeed.Models;
using System.Text; using System.Text;
using AS1024.GeoFeed.Core.GeoFeedLocalCache;
using AS1024.GeoFeed.Core.Tools;
namespace AS1024.GeoFeed.Controllers namespace AS1024.GeoFeed.Controllers
{ {
@@ -17,13 +18,16 @@ namespace AS1024.GeoFeed.Controllers
private readonly IMemoryCache memoryCache; private readonly IMemoryCache memoryCache;
private readonly IWebHostEnvironment environment; private readonly IWebHostEnvironment environment;
private readonly ILogger<GeofeedController> logger; private readonly ILogger<GeofeedController> logger;
private readonly GeoFeedCacheDbContext dbContext;
private const string GeoFeedCacheKey = "GeoFeedData"; private const string GeoFeedCacheKey = "GeoFeedData";
public GeofeedController(IGeoFeedProvider builder, public GeofeedController(IGeoFeedProvider builder,
IMemoryCache memoryCache, IMemoryCache memoryCache,
IWebHostEnvironment environment, IWebHostEnvironment environment,
ILogger<GeofeedController> logger) { ILogger<GeofeedController> logger,
GeoFeedCacheDbContext dbContext) {
this.logger = logger; this.logger = logger;
this.dbContext = dbContext;
this.builder = builder; this.builder = builder;
this.memoryCache = memoryCache; this.memoryCache = memoryCache;
this.environment = environment; this.environment = environment;
@@ -46,19 +50,36 @@ namespace AS1024.GeoFeed.Controllers
} }
} }
string csvContent = feed.ToGeoFeedCsv(); // Assuming ToGeoFeedCsv() returns a string in CSV format. return ReturnFile(feed);
byte[] contentBytes = Encoding.UTF8.GetBytes(csvContent); } catch (HttpRequestException ex)
string contentType = "text/csv"; {
logger.LogWarning($"Temporary failure of retrieving GeoData from upstream. {ex}");
var results =
dbContext.GeoFeedCacheEntries.ToList();
List<IPGeoFeed> cachedData = [];
results.ForEach(cachedData.Add);
return ReturnFile(cachedData);
}
return new FileContentResult(contentBytes, contentType) catch (Exception ex)
{
FileDownloadName = "geofeed.csv"
};
} catch (Exception ex)
{ {
logger.LogError($"Geofeed generation failed. Exception: {ex}"); logger.LogError($"Geofeed generation failed. Exception: {ex}");
return StatusCode(500); return StatusCode(500);
} }
}
[NonAction]
private IActionResult ReturnFile(List<IPGeoFeed>? feed)
{
string csvContent = feed.ToGeoFeedCsv(); // Assuming ToGeoFeedCsv() returns a string in CSV format.
byte[] contentBytes = Encoding.UTF8.GetBytes(csvContent);
string contentType = "text/csv";
return new FileContentResult(contentBytes, contentType)
{
FileDownloadName = "geofeed.csv"
};
} }
} }
} }

View File

@@ -0,0 +1,54 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM alpine:latest AS base
ENV \
# UID of the non-root user 'app'
APP_UID=1654 \
# Configure web servers to bind to port 8080 when present
ASPNETCORE_HTTP_PORTS=8080 \
# Enable detection of running in a container
DOTNET_RUNNING_IN_CONTAINER=true \
# Set the invariant mode since ICU package isn't included (see https://github.com/dotnet/announcements/issues/20)
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true
RUN apk add --upgrade --no-cache \
ca-certificates-bundle \
\
# .NET dependencies
libgcc \
libssl3 \
libstdc++ \
zlib
# Create a non-root user and group
RUN addgroup \
--gid=$APP_UID \
app \
&& adduser \
--uid=$APP_UID \
--ingroup=app \
--disabled-password \
app
WORKDIR /app
USER app
EXPOSE 8080
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
ARG BUILD_CONFIGURATION=Release
ARG TARGETARCH
WORKDIR /src
COPY ["AS1024.GeoFeed/AS1024.GeoFeed.csproj", "AS1024.GeoFeed/"]
RUN dotnet restore "./AS1024.GeoFeed/./AS1024.GeoFeed.csproj" -a $TARGETARCH
COPY . .
WORKDIR "/src/AS1024.GeoFeed"
RUN dotnet build "./AS1024.GeoFeed.csproj" -c $BUILD_CONFIGURATION -o /app/build -a $TARGETARCH /p:StaticLink=true
FROM --platform=$BUILDPLATFORM build AS publish
ARG BUILD_CONFIGURATION=Release
ARG TARGETARCH
RUN dotnet publish "./AS1024.GeoFeed.csproj" -c $BUILD_CONFIGURATION -o /app/publish -a $TARGETARCH --self-contained
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["/app/AS1024.GeoFeed"]

View File

@@ -1,5 +1,8 @@
using AS1024.GeoFeed.GeoFeedBuilder; using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Interfaces; using AS1024.GeoFeed.Core.GeoFeedPreloader;
using AS1024.GeoFeed.Core.GeoFeedLocalCache;
using AS1024.GeoFeed.Core.GeoFeedProviders;
using Microsoft.EntityFrameworkCore;
namespace AS1024.GeoFeed namespace AS1024.GeoFeed
{ {
@@ -11,6 +14,11 @@ namespace AS1024.GeoFeed
builder.Services.AddHostedService<PreLoadGeoFeed>(); builder.Services.AddHostedService<PreLoadGeoFeed>();
builder.Services.AddTransient<IGeoFeedProvider, NetBoxGeoFeedProvider>(); builder.Services.AddTransient<IGeoFeedProvider, NetBoxGeoFeedProvider>();
builder.Services.AddHostedService<GeoFeedCacheService>();
builder.Services.AddDbContext<GeoFeedCacheDbContext>(options =>
{
options.UseSqlite(builder.Configuration.GetConnectionString("LocalFeedCache"));
});
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
builder.Services.AddMemoryCache(); builder.Services.AddMemoryCache();
// Add services to the container. // Add services to the container.

View File

@@ -5,6 +5,9 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"ConnectionStrings": {
"LocalFeedCache": "Data Source=localcache.db"
},
"AllowedHosts": "*", "AllowedHosts": "*",
"APIKey": "", "APIKey": "",
"NetBoxHost": "" "NetBoxHost": ""

View File

@@ -1,6 +1,6 @@
BSD 3-Clause License BSD 3-Clause License
Copyright (c) [year], [fullname] Copyright (c) 2024, 12393239 Canada Inc.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:

View File

@@ -10,6 +10,7 @@ The application is implemented in C# using .NET 8.0, ensuring a robust and moder
- **GeoFeed Generation**: Dynamically generates a geolocation feed in CSV format as specified in RFC 8805. - **GeoFeed Generation**: Dynamically generates a geolocation feed in CSV format as specified in RFC 8805.
- **Caching Mechanism**: Implements an efficient caching strategy to reduce redundant API calls and enhance performance. - **Caching Mechanism**: Implements an efficient caching strategy to reduce redundant API calls and enhance performance.
- **Local Disk Fallback Caching Mechanism**: In the event of a failure to communicate with the NetBox instance specified, the web app will return data that is locally cached inside a SQLite database.
- **Secure Communication**: Ensures secure data retrieval from NetBox over HTTPS. - **Secure Communication**: Ensures secure data retrieval from NetBox over HTTPS.
## Configuration ## Configuration
@@ -18,9 +19,22 @@ The application requires the following configuration variables to be set:
1. **APIKey**: This is the API key used for authenticating with the NetBox API. Ensure this key has the necessary permissions to access the required resources. 1. **APIKey**: This is the API key used for authenticating with the NetBox API. Ensure this key has the necessary permissions to access the required resources.
2. **NetBoxHost**: The hostname of the NetBox instance from which the application retrieves data. For example, `netbox.example.com`. 2. **NetBoxHost**: The hostname of the NetBox instance from which the application retrieves data. For example, `netbox.example.com`.
3. **LocalFeedCache**: This connection string is for a local SQLite Database that caches the geofeed data from Netbox.
These variables can be set in your application's configuration file or through environment variables, depending on your deployment strategy. These variables can be set in your application's configuration file or through environment variables, depending on your deployment strategy.
## NetBox Custom Fields
Ensure that your NetBox instance is configured with the following custom fields:
- `geoloc_city`: (Text) Represents the city and is not required to be filled in.
- `geoloc_country`: (Selection) Represents the country and is not required to be filled in.
- `geoloc_has_location`: (Boolean) Indicates if there is geolocation data available and is required.
- `geoloc_postal_code`: (Text) Represents the postal code and is not required to be filled in.
- `geoloc_region`: (Selection) Represents the region and is not required to be filled in.
These fields are critical for the application to accurately retrieve and format geolocation data.
## Getting Started ## Getting Started
To build and run the application, follow these steps: To build and run the application, follow these steps:
@@ -32,10 +46,6 @@ To build and run the application, follow these steps:
5. After a successful build, you can start the application by running `dotnet run`. 5. After a successful build, you can start the application by running `dotnet run`.
6. The application will start, and you can access the endpoints as specified. 6. The application will start, and you can access the endpoints as specified.
## Docker Build
A Dockerfile is provided for your convienence. This has not been tested as of yet.
## Endpoints ## Endpoints
The application provides the following key endpoints: The application provides the following key endpoints: