Skip to content

Commit

Permalink
Add lab 4 src starting point (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
DamianEdwards authored Feb 5, 2024
1 parent e2a65d6 commit 1dc1b0f
Show file tree
Hide file tree
Showing 232 changed files with 6,940 additions and 40 deletions.
37 changes: 0 additions & 37 deletions labs/3-Add-Identity/src/Keycloak/data/import/eshop-realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,6 @@
"attributes" : { }
} ],
"security-admin-console" : [ ],
"orderingswaggerui" : [ ],
"admin-cli" : [ ],
"account-console" : [ ],
"broker" : [ {
Expand Down Expand Up @@ -561,42 +560,6 @@
"nodeReRegistrationTimeout" : 0,
"defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
}, {
"id" : "6bbe9167-4ac5-49e3-a0ea-06fa6b9fe56c",
"clientId" : "orderingswaggerui",
"name" : "Ordering Swagger UI",
"description" : "",
"rootUrl" : "${ORDERINGAPI_HTTP}",
"adminUrl" : "${ORDERINGAPI_HTTP}",
"baseUrl" : "${ORDERINGAPI_HTTP}",
"surrogateAuthRequired" : false,
"enabled" : true,
"alwaysDisplayInConsole" : false,
"clientAuthenticatorType" : "client-secret",
"redirectUris" : [ "${ORDERINGAPI_HTTP}/*" ],
"webOrigins" : [ "${ORDERINGAPI_HTTP}" ],
"notBefore" : 0,
"bearerOnly" : false,
"consentRequired" : false,
"standardFlowEnabled" : true,
"implicitFlowEnabled" : true,
"directAccessGrantsEnabled" : true,
"serviceAccountsEnabled" : false,
"publicClient" : true,
"frontchannelLogout" : true,
"protocol" : "openid-connect",
"attributes" : {
"oidc.ciba.grant.enabled" : "false",
"post.logout.redirect.uris" : "+",
"oauth2.device.authorization.grant.enabled" : "false",
"backchannel.logout.session.required" : "true",
"backchannel.logout.revoke.offline.tokens" : "false"
},
"authenticationFlowBindingOverrides" : { },
"fullScopeAllowed" : true,
"nodeReRegistrationTimeout" : -1,
"defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
}, {
"id" : "348d0c1d-6d87-4975-b5b1-d3f7ca245cd0",
"clientId" : "realm-management",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
<div class="dropdown-menu">
<span class="dropdown-button"><img role="presentation" src="icons/user.svg" /></span>
<div class="dropdown-content">
<a class="dropdown-item" href="user/orders">My orders</a>
<form class="dropdown-item" method="post" action="user/logout" @formname="logout" @onsubmit="LogOutAsync">
<AntiforgeryToken />
<button type="submit">Log out</button>
Expand Down
214 changes: 214 additions & 0 deletions labs/4-Add-Shopping-Basket/src/Catalog.API/Apis/CatalogApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
using eShop.Catalog.API;
using eShop.Catalog.API.Model;
using eShop.Catalog.Data;

namespace Microsoft.AspNetCore.Builder;

public static class CatalogApi
{
private static readonly FileExtensionContentTypeProvider _fileContentTypeProvider = new();

public static IEndpointRouteBuilder MapCatalogApi(this IEndpointRouteBuilder app)
{
// Routes for querying catalog items.
app.MapGet("/items", GetAllItems);
app.MapGet("/items/by", GetItemsByIds);
app.MapGet("/items/{id:int}", GetItemById);
app.MapGet("/items/by/{name:minlength(1)}", GetItemsByName);
app.MapGet("/items/{catalogItemId:int}/pic", GetItemPictureById);

// Routes for resolving catalog items by type and brand.
app.MapGet("/items/type/{typeId}/brand/{brandId?}", GetItemsByBrandAndTypeId);
app.MapGet("/items/type/all/brand/{brandId:int?}", GetItemsByBrandId);
app.MapGet("/catalogtypes", async (CatalogDbContext context) => await context.CatalogTypes.OrderBy(x => x.Type).AsNoTracking().ToListAsync());
app.MapGet("/catalogbrands", async (CatalogDbContext context) => await context.CatalogBrands.OrderBy(x => x.Brand).AsNoTracking().ToListAsync());

return app;
}

public static async Task<Results<Ok<PaginatedItems<CatalogItem>>, BadRequest<string>>> GetAllItems(
[AsParameters] PaginationRequest paginationRequest,
[AsParameters] CatalogServices services)
{
var pageSize = paginationRequest.PageSize;
var pageIndex = paginationRequest.PageIndex;

var totalItems = await services.DbContext.CatalogItems
.LongCountAsync();

var itemsOnPage = await services.DbContext.CatalogItems
.OrderBy(c => c.Name)
.Skip(pageSize * pageIndex)
.Take(pageSize)
.AsNoTracking()
.ToListAsync();

ChangeUriPlaceholder(services.Options.Value, itemsOnPage);

return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));
}

public static async Task<Ok<List<CatalogItem>>> GetItemsByIds(
[AsParameters] CatalogServices services,
int[] ids)
{
var items = await services.DbContext.CatalogItems
.Where(item => ids.Contains(item.Id))
.AsNoTracking()
.ToListAsync();

ChangeUriPlaceholder(services.Options.Value, items);

return TypedResults.Ok(items);
}

public static async Task<Results<Ok<CatalogItem>, NotFound, BadRequest<string>>> GetItemById(
[AsParameters] CatalogServices services,
int id)
{
if (id <= 0)
{
return TypedResults.BadRequest("Id is not valid.");
}

var item = await services.DbContext.CatalogItems
.Include(ci => ci.CatalogBrand)
.AsNoTracking()
.SingleOrDefaultAsync(ci => ci.Id == id);

if (item == null)
{
return TypedResults.NotFound();
}

item.PictureUri = services.Options.Value.GetPictureUrl(item.Id);

return TypedResults.Ok(item);
}

public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByName(
[AsParameters] PaginationRequest paginationRequest,
[AsParameters] CatalogServices services,
string name)
{
var pageSize = paginationRequest.PageSize;
var pageIndex = paginationRequest.PageIndex;

var totalItems = await services.DbContext.CatalogItems
.Where(c => c.Name.StartsWith(name))
.LongCountAsync();

var itemsOnPage = await services.DbContext.CatalogItems
.Where(c => c.Name.StartsWith(name))
.Skip(pageSize * pageIndex)
.Take(pageSize)
.AsNoTracking()
.ToListAsync();

ChangeUriPlaceholder(services.Options.Value, itemsOnPage);

return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));
}

public static async Task<Results<NotFound, PhysicalFileHttpResult>> GetItemPictureById(CatalogDbContext context, IWebHostEnvironment environment, int catalogItemId)
{
var item = await context.CatalogItems
.AsNoTracking()
.FirstOrDefaultAsync(i => i.Id == catalogItemId);

if (item is null)
{
return TypedResults.NotFound();
}

var path = GetFullPath(environment.ContentRootPath, item.PictureFileName);

var imageFileExtension = Path.GetExtension(item.PictureFileName);
_fileContentTypeProvider.TryGetContentType(imageFileExtension, out var contentType);
var lastModified = File.GetLastWriteTimeUtc(path);

return TypedResults.PhysicalFile(path, contentType, lastModified: lastModified);
}

public static async Task<Results<BadRequest<string>, RedirectToRouteHttpResult, Ok<PaginatedItems<CatalogItem>>>> GetItemsBySemanticRelevance(
[AsParameters] PaginationRequest paginationRequest,
[AsParameters] CatalogServices services,
string text)
{
return await GetItemsByName(paginationRequest, services, text);
}

public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByBrandAndTypeId(
[AsParameters] PaginationRequest paginationRequest,
[AsParameters] CatalogServices services,
int typeId,
int? brandId)
{
var pageSize = paginationRequest.PageSize;
var pageIndex = paginationRequest.PageIndex;

var query = services.DbContext.CatalogItems.AsQueryable();
query = query.Where(c => c.CatalogTypeId == typeId);

if (brandId is not null)
{
query = query.Where(c => c.CatalogBrandId == brandId);
}

var totalItems = await query
.LongCountAsync();

var itemsOnPage = await query
.Skip(pageSize * pageIndex)
.Take(pageSize)
.AsNoTracking()
.ToListAsync();

ChangeUriPlaceholder(services.Options.Value, itemsOnPage);

return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));
}

public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByBrandId(
[AsParameters] PaginationRequest paginationRequest,
[AsParameters] CatalogServices services,
int? brandId)
{
var pageSize = paginationRequest.PageSize;
var pageIndex = paginationRequest.PageIndex;

var query = (IQueryable<CatalogItem>)services.DbContext.CatalogItems;

if (brandId is not null)
{
query = query.Where(ci => ci.CatalogBrandId == brandId);
}

var totalItems = await query
.LongCountAsync();

var itemsOnPage = await query
.Skip(pageSize * pageIndex)
.Take(pageSize)
.AsNoTracking()
.ToListAsync();

ChangeUriPlaceholder(services.Options.Value, itemsOnPage);

return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));
}

private static void ChangeUriPlaceholder(CatalogOptions options, List<CatalogItem> items)
{
foreach (var item in items)
{
item.PictureUri = options.GetPictureUrl(item.Id);
}
}

public static string GetFullPath(string contentRootPath, string pictureFileName) =>
Path.Combine(contentRootPath, "Pics", pictureFileName);
}
15 changes: 15 additions & 0 deletions labs/4-Add-Shopping-Basket/src/Catalog.API/Catalog.API.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>eShop.Catalog.API</RootNamespace>
<UserSecretsId>d1b521ec-3411-4d39-98c6-8509466ed471</UserSecretsId>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Catalog.Data\Catalog.Data.csproj" />
<ProjectReference Include="..\eShop.ServiceDefaults\eShop.ServiceDefaults.csproj" />
</ItemGroup>

</Project>
10 changes: 10 additions & 0 deletions labs/4-Add-Shopping-Basket/src/Catalog.API/Catalog.API.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@Catalog.API_HostAddress = http://localhost:5222


GET {{Catalog.API_HostAddress}}/api/v1/catalog/items

###

GET {{Catalog.API_HostAddress}}/api/v1/catalog/items/type/1/brand/2

###
18 changes: 18 additions & 0 deletions labs/4-Add-Shopping-Basket/src/Catalog.API/CatalogOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Globalization;

namespace eShop.Catalog.API;

public class CatalogOptions
{
public string ApiBasePath { get; set; } = "/api/v1/catalog/";

public string? PicBaseAddress { get; set; } // Set by hosting environment

public string PicBasePathFormat { get; set; } = "items/{0}/pic/";

public string GetPictureUrl(int catalogItemId)
{
// PERF: Not ideal
return PicBaseAddress + ApiBasePath + string.Format(CultureInfo.InvariantCulture, PicBasePathFormat, catalogItemId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using eShop.Catalog.API;
using eShop.Catalog.Data;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.Hosting;

public static class HostingExtensions
{
public static void AddApplicationServices(this IHostApplicationBuilder builder)
{
builder.AddNpgsqlDbContext<CatalogDbContext>("CatalogDB");

builder.Services.Configure<CatalogOptions>(builder.Configuration.GetSection(nameof(CatalogOptions)));
}

public static TOptions GetOptions<TOptions>(this IHost host)
where TOptions : class, new()
{
return host.Services.GetRequiredService<IOptions<TOptions>>().Value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.Extensions.Options;
using eShop.Catalog.Data;

namespace eShop.Catalog.API.Model;

public readonly struct CatalogServices(CatalogDbContext dbContext, IOptions<CatalogOptions> options, ILogger<CatalogServices> logger)
{
public CatalogDbContext DbContext { get; } = dbContext;

public IOptions<CatalogOptions> Options { get; } = options;

public ILogger<CatalogServices> Logger { get; } = logger;
};
12 changes: 12 additions & 0 deletions labs/4-Add-Shopping-Basket/src/Catalog.API/Model/PaginatedItems.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace eShop.Catalog.API.Model;

public class PaginatedItems<TEntity>(int pageIndex, int pageSize, long count, IEnumerable<TEntity> data) where TEntity : class
{
public int PageIndex { get; } = pageIndex;

public int PageSize { get; } = pageSize;

public long Count { get; } = count;

public IEnumerable<TEntity> Data { get;} = data;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace eShop.Catalog.API.Model;

public readonly struct PaginationRequest(int pageSize = 0, int pageIndex = 0)
{
public readonly int PageSize { get; } = pageSize;

public readonly int PageIndex { get; } = pageIndex;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
6 changes: 6 additions & 0 deletions labs/4-Add-Shopping-Basket/src/Catalog.API/Program.Testing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Require a public Program class to implement the
// fixture for the WebApplicationFactory in the
// integration tests. Using IVT is not sufficient
// in this case, because the accessibility of the
// `Program` type is checked.
public partial class Program { }
Loading

0 comments on commit 1dc1b0f

Please sign in to comment.