Web Api layout

This commit is contained in:
Jöran Malek 2024-02-07 22:16:59 +01:00
parent 5619093f41
commit da6d5576bf
32 changed files with 515 additions and 29 deletions

View file

@ -7,6 +7,12 @@
"commands": [
"dotnet-ef"
]
},
"dotnet-aspnet-codegenerator": {
"version": "8.0.0",
"commands": [
"dotnet-aspnet-codegenerator"
]
}
}
}

View file

@ -29,7 +29,7 @@ tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
insert_final_newline = true
#### .NET Coding Conventions ####
[*.{cs,vb}]

View file

@ -4,10 +4,16 @@
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" Version="7.0.15" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1" />
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageVersion Include="System.IO.Hashing" Version="8.0.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,36 @@
using InkForge.Api.Data.Infrastructure;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace InkForge.Api.Data;
public class ApiDbcontext(
DbContextOptions<ApiDbcontext> options
) : IdentityDbContext<IdentityUser>(options)
{
public DbSet<WorkspaceEntity> Workspaces { get; set; } = default!;
public DbSet<WorkspaceVersionEntity> WorkspaceVersions { get; set; } = default!;
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<WorkspaceEntity>(options =>
{
options.OwnsOne(m => m.Value);
options.HasKey(m => m.Id);
});
builder.Entity<WorkspaceVersionEntity>(options =>
{
options.OwnsOne(m => m.Value);
options.HasKey(m => m.Version);
options.HasIndex(nameof(WorkspaceVersionEntity.Id), nameof(WorkspaceVersionEntity.Version)).IsUnique();
});
}
}

View file

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Identity;
namespace InkForge.Api.Data.Domain;
public class Workspace
{
public string Name { get; set; } = default!;
public DateTimeOffset Created { get; set; }
public IdentityUser Owner { get; set; } = default!;
public DateTimeOffset Updated { get; set; }
public DateTimeOffset? Deleted { get; set; }
}

View file

@ -0,0 +1,9 @@
using InkForge.Api.Data.Domain;
using InkForge.Data;
namespace InkForge.Api.Data.Infrastructure
{
public class WorkspaceEntity : Entity<Workspace, int>;
public class WorkspaceVersionEntity : VersionedEntity<Workspace, int>;
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\shared\InkForge.Data\InkForge.Data.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,4 @@
@{
Layout = "/Pages/Shared/_Layout.cshtml";
}

View file

@ -1,15 +0,0 @@
using Duende.IdentityServer.EntityFramework.Options;
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace InkForge.Api.Data;
public class ApiDbcontext(
DbContextOptions options,
IOptions<OperationalStoreOptions> operationalStoreOptions
) : ApiAuthorizationDbContext<IdentityUser>(options, operationalStoreOptions)
{
}

View file

@ -9,11 +9,16 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="System.IO.Hashing" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\InkForge.Api.Data\InkForge.Api.Data.csproj" />
<ProjectReference Include="..\migrations\InkForge.Api.Sqlite\InkForge.Api.Sqlite.csproj" />
<ProjectReference Include="..\shared\InkForge.Data\InkForge.Data.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,94 @@
@using Microsoft.AspNetCore.Hosting
@using Microsoft.AspNetCore.Mvc.ViewEngines
@inject IWebHostEnvironment Environment
@inject ICompositeViewEngine Engine
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - InkForge.Api</title>
<environment include="Development">
<link rel="stylesheet" href="~/Identity/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/Identity/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css"
integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous"
asp-fallback-href="~/Identity/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="~/Identity/css/site.css" asp-append-version="true" />
</environment>
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" href="~/">InkForge.Api</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
@{
var result = Engine.FindView(ViewContext, "_LoginPartial", isMainPage: false);
}
@if (result.Success)
{
await Html.RenderPartialAsync("_LoginPartial");
}
else
{
throw new InvalidOperationException("The default Identity UI layout requires a partial view '_LoginPartial' " +
"usually located at '/Pages/_LoginPartial' or at '/Views/Shared/_LoginPartial' to work. Based on your configuration " +
$"we have looked at it in the following locations: {System.Environment.NewLine}{string.Join(System.Environment.NewLine, result.SearchedLocations)}.");
}
</div>
</div>
</nav>
</header>
<div class="container">
<partial name="_CookieConsentPartial" optional />
<main role="main" class="pb-1">
@RenderBody()
</main>
</div>
<footer class="footer border-top pl-3 text-muted">
<div class="container">
&copy; 2024 - InkForge.Api
@{
var foundPrivacy = Url.Page("/Privacy", new { area = "" });
}
@if (foundPrivacy != null)
{
<a asp-area="" asp-page="/Privacy">Privacy</a>
}
</div>
</footer>
<environment include="Development">
<script src="~/Identity/lib/jquery/dist/jquery.js"></script>
<script src="~/Identity/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
<script src="~/Identity/js/site.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"
asp-fallback-src="~/Identity/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2">
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js"
asp-fallback-src="~/Identity/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj">
</script>
<script src="~/Identity/js/site.js" asp-append-version="true"></script>
</environment>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View file

@ -0,0 +1,27 @@
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<a id="manage" class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @UserManager.GetUserName(User)!</a>
</li>
<li class="nav-item">
<form id="logoutForm" class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })">
<button id="logout" type="submit" class="nav-link btn btn-link text-dark border-0">Logout</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" id="register" asp-area="Identity" asp-page="/Account/Register">Register</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" id="login" asp-area="Identity" asp-page="/Account/Login">Login</a>
</li>
}
</ul>

View file

@ -0,0 +1,18 @@
<environment include="Development">
<script src="~/Identity/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</environment>
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.17.0/jquery.validate.min.js"
asp-fallback-src="~/Identity/lib/jquery-validation/dist/jquery.validate.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator"
crossorigin="anonymous"
integrity="sha384-rZfj/ogBloos6wzLGpPkkOr/gpkBNLZ6b6yLy4o+ok+t/SAKlL5mvXLr0OXNi1Hp">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"
asp-fallback-src="~/Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"
crossorigin="anonymous"
integrity="sha384-R3vNCHsZ+A2Lo3d5A6XNP7fdQkeswQWTIPfiYwSpEP3YV079R+93YzTeZRah7f/F">
</script>
</environment>

View file

@ -0,0 +1,6 @@
@using Microsoft.AspNetCore.Identity
@using InkForge.Api.Data
InkForge.Api.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View file

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

View file

@ -1,17 +1,31 @@
using Microsoft.AspNetCore.Identity;
using InkForge.Api.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddHttpClient();
builder.Services.AddIdentityApiEndpoints<IdentityUser>()
.AddEntityFrameworkStores<ApiDbcontext>()
.AddDefaultUI().AddDefaultTokenProviders();
var provider = builder.Configuration.GetValue<string>("DbProvider");
builder.Services.AddDbContext<ApiDbcontext>(options => _ = provider switch
{
"Sqlite" => options.UseSqlite(
builder.Configuration.GetConnectionString("AuthDb"),
x => x.MigrationsAssembly("InkForge.Api.Sqlite")
),
_ => throw new Exception($"Invalid Provider: {provider}")
});
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddIdentityApiEndpoints<IdentityUser>()
.AddEntityFrameworkStores<ApiDbcontext>();
var app = builder.Build();
@ -23,11 +37,15 @@ if (app.Environment.IsDevelopment())
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapIdentityApi<IdentityUser>();
app.MapControllers();
app.MapRazorPages();
app.Run();

View file

@ -4,5 +4,10 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"DbProvider": "Sqlite",
"ConnectionStrings": {
"AuthDb": "Data Source=Identity.db",
"WorkspaceDbTemplate": "Data Source=Workspaces/{WorkspaceId}.db"
}
}

View file

@ -5,5 +5,10 @@
"Microsoft.AspNetCore": "Warning"
}
},
"DbProvider": "",
"ConnectionStrings": {
"AuthDb": "",
"WorkspaceDbTemplate": ""
},
"AllowedHosts": "*"
}

View file

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>InkForge.Desktop</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

View file

@ -0,0 +1,2 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

View file

@ -0,0 +1,23 @@
using InkForge.Api.Data;
using Microsoft.EntityFrameworkCore;
namespace InkForge.Migrations;
public class ApiDbContextFactory : MigratingDbContextFactory<ApiDbcontext>
{
protected override void Configure(
DbContextOptionsBuilder<ApiDbcontext> optionsBuilder,
string connectionString,
string provider
) => _ = provider switch
{
"Sqlite" => optionsBuilder.UseSqlite(connectionString,
m => m.MigrationsAssembly("InkForge.Api.Sqlite")
),
_ => throw new Exception($"Invalid DbProvider: {provider}")
};
protected override ApiDbcontext CreateDbContext(DbContextOptions<ApiDbcontext> options) => new(options);
}

View file

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\migrations\InkForge.Api.Sqlite\InkForge.Api.Sqlite.csproj" />
<ProjectReference Include="..\..\shared\migrations\InkForge.Sqlite\InkForge.Sqlite.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace InkForge.Migrations;
public abstract class MigratingDbContextFactory<T> : IDesignTimeDbContextFactory<T>
where T : DbContext
{
public T CreateDbContext(string[] args)
{
var configuration = new ConfigurationBuilder()
.AddCommandLine(args)
.Build();
var options = new DbContextOptionsBuilder<T>();
switch (configuration.GetValue<string>("DbProvider"))
{
case null:
throw new Exception("DbProvider not set.");
case { } provider:
Configure(options, configuration.GetConnectionString("DefaultConnection")!, provider);
break;
}
return CreateDbContext(options.Options);
}
protected abstract void Configure(DbContextOptionsBuilder<T> optionsBuilder, string connectionString, string provider);
protected abstract T CreateDbContext(DbContextOptions<T> options);
}

View file

@ -0,0 +1,23 @@
using InkForge.Data;
using Microsoft.EntityFrameworkCore;
namespace InkForge.Migrations;
public class NoteDbContextFactory : MigratingDbContextFactory<NoteDbContext>
{
protected override void Configure(
DbContextOptionsBuilder<NoteDbContext> optionsBuilder,
string connectionString,
string provider
) => _ = provider switch
{
"Sqlite" => optionsBuilder.UseSqlite(connectionString,
m => m.MigrationsAssembly("InkForge.Sqlite")
),
_ => throw new Exception($"Invalid DbProvider: {provider}")
};
protected override NoteDbContext CreateDbContext(DbContextOptions<NoteDbContext> options) => new(options);
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\InkForge.Api.Data\InkForge.Api.Data.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,18 @@
using InkForge.Data.Infrastructure;
namespace InkForge.Data.Domain;
public class Note
{
public DateTimeOffset Created { get; set; }
public NoteEntity? Parent { get; set; }
public string Name { get; set; } = default!;
public DateTimeOffset Updated { get; set; }
public DateTimeOffset? Deleted { get; set; }
public Blob Content { get; set; } = default!;
}

View file

@ -0,0 +1,8 @@
namespace InkForge.Data;
public class Blob
{
public string Id { get; set; } = default!;
public byte[] Content { get; set; } = default!;
}

View file

@ -0,0 +1,25 @@
using System.Numerics;
namespace InkForge.Data
{
public abstract class ValueEntity<TEntity>
{
public TEntity Value { get; set; } = default!;
}
public abstract class Entity<TEntity, TKey>
: ValueEntity<TEntity>
where TKey : struct, INumber<TKey>
{
public TKey? Id { get; set; }
}
public abstract class VersionedEntity<TEntity, TKey>
: ValueEntity<TEntity>
where TKey : struct, INumber<TKey>
{
public TKey Id { get; set; }
public int? Version { get; set; }
}
}

View file

@ -0,0 +1,8 @@
using InkForge.Data.Domain;
namespace InkForge.Data.Infrastructure
{
public class NoteEntity : Entity<Note, int>;
public class NoteVersionEntity : VersionedEntity<Note, int>;
}

View file

@ -7,4 +7,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,34 @@
using InkForge.Data.Infrastructure;
using Microsoft.EntityFrameworkCore;
namespace InkForge.Data;
public class NoteDbContext(
DbContextOptions<NoteDbContext> options
) : DbContext(options)
{
public DbSet<Blob> Blobs { get; set; } = default!;
public DbSet<NoteEntity> Notes { get; set; } = default!;
public DbSet<NoteVersionEntity> NoteVersions { get; set; } = default!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<NoteEntity>(options =>
{
options.OwnsOne(m => m.Value);
options.HasKey(m => m.Id);
});
modelBuilder.Entity<NoteVersionEntity>(options =>
{
options.OwnsOne(m => m.Value);
options.Property(m => m.Id).IsRequired();
options.HasKey(m => m.Version);
options.HasIndex(nameof(NoteVersionEntity.Id), nameof(NoteVersionEntity.Version)).IsUnique();
});
}
}

View file

@ -7,4 +7,12 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\InkForge.Data\InkForge.Data.csproj" />
</ItemGroup>
</Project>