Skip to content

Commit

Permalink
Feat: now using keyed services (#167)
Browse files Browse the repository at this point in the history
* Feat: now using keyed services

* chore: ran cleanup on new code

---------

Co-authored-by: Tom Whittington <[email protected]>
  • Loading branch information
ThomasWhittington and Tom Whittington authored Oct 7, 2024
1 parent 678cc91 commit 0423354
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 138 deletions.
7 changes: 5 additions & 2 deletions src/Dfe.ContentSupport.Web/Controllers/CacheController.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using Dfe.ContentSupport.Web.Models.Mapped;
using Dfe.ContentSupport.Web.Extensions;
using Dfe.ContentSupport.Web.Models.Mapped;
using Dfe.ContentSupport.Web.Services;
using Microsoft.AspNetCore.Mvc;

namespace Dfe.ContentSupport.Web.Controllers;

[Route("api/[controller]")]
[ApiController]
public class CacheController(ICacheService<List<CsPage>> cache) : ControllerBase
public class CacheController(
[FromKeyedServices(WebApplicationBuilderExtensions.ContentAndSupportServiceKey)]
ICacheService<List<CsPage>> cache) : ControllerBase
{
[HttpGet]
[Route("clear")]
Expand Down
27 changes: 19 additions & 8 deletions src/Dfe.ContentSupport.Web/Controllers/ContentController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using Dfe.ContentSupport.Web.Extensions;
using Dfe.ContentSupport.Web.Services;
using Dfe.ContentSupport.Web.ViewModels;
using Microsoft.AspNetCore.Authorization;
Expand All @@ -8,23 +9,32 @@ namespace Dfe.ContentSupport.Web.Controllers;

[Route("/content")]
[AllowAnonymous]
public class ContentController(IContentService contentService, ILayoutService layoutService, ILogger<ContentController> logger)
public class ContentController(
[FromKeyedServices(WebApplicationBuilderExtensions.ContentAndSupportServiceKey)]
IContentService contentService,
[FromKeyedServices(WebApplicationBuilderExtensions.ContentAndSupportServiceKey)]
ILayoutService layoutService,
ILogger<ContentController> logger)
: Controller
{
public const string ErrorActionName = "error";

[HttpGet("{slug}/{page?}")]
public async Task<IActionResult> Index(string slug, string page = "", bool isPreview = false, [FromQuery] List<string>? tags = null)
public async Task<IActionResult> Index(string slug, string page = "", bool isPreview = false,
[FromQuery] List<string>? tags = null)
{
if (!ModelState.IsValid)
{
logger.LogError("Invalid model state received for {Controller} {Action} with slug {Slug}", nameof(ContentController), nameof(Index), slug);
logger.LogError(
"Invalid model state received for {Controller} {Action} with slug {Slug}",
nameof(ContentController), nameof(Index), slug);
return RedirectToAction(ErrorActionName);
}

if (string.IsNullOrEmpty(slug))
{
logger.LogError("No slug received for C&S {Controller} {Action}", nameof(ContentController), nameof(Index));
logger.LogError("No slug received for C&S {Controller} {Action}",
nameof(ContentController), nameof(Index));
return RedirectToAction(ErrorActionName);
}

Expand All @@ -33,7 +43,8 @@ public async Task<IActionResult> Index(string slug, string page = "", bool isPre
var resp = await contentService.GetContent(slug, isPreview);
if (resp is null)
{
logger.LogError("Failed to load content for C&S page {Slug}; no content received.", slug);
logger.LogError("Failed to load content for C&S page {Slug}; no content received.",
slug);
return RedirectToAction(ErrorActionName);
}

Expand All @@ -48,12 +59,12 @@ public async Task<IActionResult> Index(string slug, string page = "", bool isPre
return RedirectToAction(ErrorActionName);
}
}


[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel
{ RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
{ RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
7 changes: 4 additions & 3 deletions src/Dfe.ContentSupport.Web/Controllers/SitemapController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Dfe.ContentSupport.Web.Models.Mapped;
using Dfe.ContentSupport.Web.Extensions;
using Dfe.ContentSupport.Web.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -7,9 +7,10 @@ namespace Dfe.ContentSupport.Web.Controllers;

[Route("/sitemap")]
[AllowAnonymous]
public class SitemapController(IContentService contentfulService) : Controller
public class SitemapController(
[FromKeyedServices(WebApplicationBuilderExtensions.ContentAndSupportServiceKey)]
IContentService contentfulService) : Controller
{

[HttpGet]
[Route("/sitemap.xml")]
public async Task<IActionResult> Sitemap()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
using Polly;
using System.Net;
using Polly;
using Polly.Extensions.Http;

namespace Dfe.ContentSupport.Web.Extensions;

public static class HttpClientPolicyExtensions
public static class CsHttpClientPolicyExtensions
{
public static void AddRetryPolicy(IHttpClientBuilder builder) =>
public static void AddRetryPolicy(IHttpClientBuilder builder)
{
builder
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddPolicyHandler(GetRetryPolicy());
}

public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy() =>
HttpPolicyExtensions.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == HttpStatusCode.NotFound)
.WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,58 @@ namespace Dfe.ContentSupport.Web.Extensions;

public static class WebApplicationBuilderExtensions
{
public const string ContentAndSupportServiceKey = "content-and-support";

public static void InitCsDependencyInjection(this WebApplicationBuilder app)
{
app.Services.Configure<TrackingOptions>(app.Configuration.GetSection("tracking"))
.AddSingleton(sp => sp.GetRequiredService<IOptions<TrackingOptions>>().Value);

app.Services.Configure<SupportedAssetTypes>(app.Configuration.GetSection("cs:supportedAssetTypes"))
app.Services
.Configure<SupportedAssetTypes>(app.Configuration.GetSection("cs:supportedAssetTypes"))
.AddSingleton(sp => sp.GetRequiredService<IOptions<SupportedAssetTypes>>().Value);

app.Services.SetupContentfulClient(app);

app.Services.AddTransient<ICacheService<List<CsPage>>, CsPagesCacheService>();
app.Services.AddTransient<IModelMapper, ModelMapper>();
app.Services.AddTransient<IContentService, ContentService>();
app.Services.AddTransient<ILayoutService, LayoutService>();
app.Services.AddKeyedTransient<ICacheService<List<CsPage>>, CsPagesCacheService>(
ContentAndSupportServiceKey);
app.Services.AddKeyedTransient<IModelMapper, ModelMapper>(ContentAndSupportServiceKey);
app.Services
.AddKeyedTransient<IContentService, ContentService>(ContentAndSupportServiceKey);
app.Services.AddKeyedTransient<ILayoutService, LayoutService>(ContentAndSupportServiceKey);

app.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Strict;
options.ConsentCookieValue = "false";
});


}

public static void SetupContentfulClient(this IServiceCollection services, WebApplicationBuilder app)
public static void SetupContentfulClient(this IServiceCollection services,
WebApplicationBuilder app)
{
app.Services.Configure<ContentfulOptions>(app.Configuration.GetSection("cs:contentful"))
.AddSingleton(sp => sp.GetRequiredService<IOptions<ContentfulOptions>>().Value);
.AddKeyedSingleton(ContentAndSupportServiceKey, (IServiceProvider sp) =>
sp.GetRequiredService<IOptions<ContentfulOptions>>().Value);

services.AddKeyedScoped<IContentfulClient, ContentfulClient>(ContentAndSupportServiceKey,
(sp, _) =>
{
var contentfulOptions =
sp.GetRequiredKeyedService<Func<IServiceProvider, ContentfulOptions>>(
ContentAndSupportServiceKey)(sp);
var httpClient = sp.GetRequiredService<HttpClient>();
return new ContentfulClient(httpClient, contentfulOptions);
});

services.AddScoped<IContentfulClient, ContentfulClient>();

if (app.Environment.EnvironmentName.Equals("e2e"))
{
services.AddScoped<IContentfulService, StubContentfulService>();
}
services.AddKeyedScoped<IContentfulService, StubContentfulService>(
ContentAndSupportServiceKey);
else
{
services.AddScoped<IContentfulService, ContentfulService>();
}


services.AddKeyedScoped<IContentfulService, ContentfulService>(
ContentAndSupportServiceKey);

HttpClientPolicyExtensions.AddRetryPolicy(services.AddHttpClient<ContentfulClient>());
CsHttpClientPolicyExtensions.AddRetryPolicy(services.AddHttpClient<ContentfulClient>());
}
}
14 changes: 6 additions & 8 deletions src/Dfe.ContentSupport.Web/Services/ContentService.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
using System.Xml.Linq;
using Dfe.ContentSupport.Web.Extensions;
using Dfe.ContentSupport.Web.Models.Mapped;
using Dfe.ContentSupport.Web.ViewModels;

namespace Dfe.ContentSupport.Web.Services;

public class ContentService(
[FromKeyedServices(WebApplicationBuilderExtensions.ContentAndSupportServiceKey)]
IContentfulService contentfulService,
[FromKeyedServices(WebApplicationBuilderExtensions.ContentAndSupportServiceKey)]
ICacheService<List<CsPage>> cache,
[FromKeyedServices(WebApplicationBuilderExtensions.ContentAndSupportServiceKey)]
IModelMapper modelMapper)
: IContentService
{
Expand Down Expand Up @@ -51,20 +55,14 @@ public async Task<List<CsPage>> GetContentSupportPages(
if (!isPreview)
{
var fromCache = cache.GetFromCache(key);
if (fromCache is not null)
{
return fromCache;
}
if (fromCache is not null) return fromCache;
}


var result = await contentfulService.GetContentSupportPages(field, value);
var pages = modelMapper.MapToCsPages(result);

if (!isPreview)
{
cache.AddToCache(key, pages);
}
if (!isPreview) cache.AddToCache(key, pages);

return pages;
}
Expand Down
10 changes: 5 additions & 5 deletions src/Dfe.ContentSupport.Web/Services/ContentfulService.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
using Contentful.Core;
using Contentful.Core.Search;
using Dfe.ContentSupport.Web.Extensions;
using Dfe.ContentSupport.Web.ViewModels;

namespace Dfe.ContentSupport.Web.Services;

public class ContentfulService(IContentfulClient client)
public class ContentfulService(
[FromKeyedServices(WebApplicationBuilderExtensions.ContentAndSupportServiceKey)]
IContentfulClient client)
: IContentfulService
{
private const int DefaultRequestDepth = 10;

private readonly IContentfulClient _client =
client ?? throw new ArgumentNullException(nameof(client));

public async Task<IEnumerable<ContentSupportPage>> GetContentSupportPages(string field,
string value, CancellationToken cancellationToken = default)
{
var builder = QueryBuilder<ContentSupportPage>.New.ContentTypeIs(nameof(ContentSupportPage))
.FieldEquals($"fields.{field}", value)
.Include(DefaultRequestDepth);

var entries = await _client.GetEntries(builder, cancellationToken);
var entries = await client.GetEntries(builder, cancellationToken);

return entries ?? Enumerable.Empty<ContentSupportPage>();
}
Expand Down
2 changes: 1 addition & 1 deletion src/Dfe.ContentSupport.Web/Services/CsPagesCacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public void AddToCache(string key, List<CsPage> item)
}

public void ClearCache()
{
{
(cache as MemoryCache)?.Clear();
}
}
2 changes: 1 addition & 1 deletion src/Dfe.ContentSupport.Web/Services/ICacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public interface ICacheService<T>
{
void AddToCache(string key,T item);
void AddToCache(string key, T item);
T? GetFromCache(string key);
void ClearCache();
}
Loading

0 comments on commit 0423354

Please sign in to comment.