Skip to content

Commit

Permalink
Feat: now using keyed services
Browse files Browse the repository at this point in the history
  • Loading branch information
Tom Whittington committed Oct 4, 2024
1 parent 678cc91 commit 316489c
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 38 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(

Check warning on line 10 in src/Dfe.ContentSupport.Web/Controllers/SitemapController.cs

View workflow job for this annotation

GitHub Actions / Build and run unit tests

Change the paths of the actions of this controller to be relative and adapt the controller route accordingly. (https://rules.sonarsource.com/csharp/RSPEC-6931)
[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
Expand Up @@ -3,7 +3,7 @@

namespace Dfe.ContentSupport.Web.Extensions;

public static class HttpClientPolicyExtensions
public static class CsHttpClientPolicyExtensions
{
public static void AddRetryPolicy(IHttpClientBuilder builder) =>
builder
Expand Down
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,
implementationFactory: (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>());
}
}
4 changes: 4 additions & 0 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
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
5 changes: 4 additions & 1 deletion src/Dfe.ContentSupport.Web/Services/StubContentfulService.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using Contentful.Core;
using Contentful.Core.Configuration;
using Contentful.Core.Models;
using Dfe.ContentSupport.Web.Extensions;
using Dfe.ContentSupport.Web.ViewModels;
using Newtonsoft.Json;

namespace Dfe.ContentSupport.Web.Services;

public class StubContentfulService(HttpClient httpClient, ContentfulOptions options)
public class StubContentfulService(
HttpClient httpClient,
[FromKeyedServices(WebApplicationBuilderExtensions.ContentAndSupportServiceKey)] ContentfulOptions options)
: ContentfulClient(httpClient, options), IContentfulService
{
public async Task<IEnumerable<ContentSupportPage>> GetContentSupportPages(string field,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public void Builder_Default_Uses_DefaultClient()


var service = builder.Services.First(o => o.ServiceType == typeof(IContentfulService));
service.ImplementationType?.Name.Should().BeEquivalentTo(nameof(ContentfulService));
service.KeyedImplementationType?.Name.Should().BeEquivalentTo(nameof(ContentfulService));
}

[Fact]
Expand All @@ -46,6 +46,6 @@ public void Builder_E2e_Uses_MockClient()
builder.InitCsDependencyInjection();

var service = builder.Services.First(o => o.ServiceType == typeof(IContentfulService));
service.ImplementationType?.Name.Should().BeEquivalentTo(nameof(StubContentfulService));
service.KeyedImplementationType?.Name.Should().BeEquivalentTo(nameof(StubContentfulService));
}
}

0 comments on commit 316489c

Please sign in to comment.