Reorganized folder structure
This commit is contained in:
176
src/HopFrame.Api/Controller/SecurityController.cs
Normal file
176
src/HopFrame.Api/Controller/SecurityController.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
using HopFrame.Api.Logic;
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Database.Models.Entries;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Models;
|
||||
using HopFrame.Security.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HopFrame.Api.Controller;
|
||||
|
||||
[ApiController]
|
||||
[Route("authentication")]
|
||||
public class SecurityController<TDbContext>(TDbContext context, IUserService users, ITokenContext tokenContext) : ControllerBase where TDbContext : HopDbContextBase {
|
||||
|
||||
[HttpPut("login")]
|
||||
public async Task<ActionResult<SingleValueResult<string>>> Login([FromBody] UserLogin login) {
|
||||
var user = await users.GetUserByEmail(login.Email);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult<SingleValueResult<string>>.NotFound("The provided email address was not found");
|
||||
|
||||
if (!await users.CheckUserPassword(user, login.Password))
|
||||
return LogicResult<SingleValueResult<string>>.Forbidden("The provided password is not correct");
|
||||
|
||||
var refreshToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.RefreshTokenType,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
var accessToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.AccessTokenType,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
|
||||
HttpContext.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.RefreshTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
HttpContext.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
await context.Tokens.AddRangeAsync(refreshToken, accessToken);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<ActionResult<SingleValueResult<string>>> Register([FromBody] UserRegister register) {
|
||||
if (register.Password.Length < 8)
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Password needs to be at least 8 characters long");
|
||||
|
||||
var allUsers = await users.GetUsers();
|
||||
if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Username or Email is already registered");
|
||||
|
||||
var user = await users.AddUser(register);
|
||||
|
||||
var refreshToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.RefreshTokenType,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
var accessToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.AccessTokenType,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
|
||||
await context.Tokens.AddRangeAsync(refreshToken, accessToken);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
HttpContext.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.RefreshTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
HttpContext.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
|
||||
}
|
||||
|
||||
[HttpGet("authenticate")]
|
||||
public async Task<ActionResult<SingleValueResult<string>>> Authenticate() {
|
||||
var refreshToken = HttpContext.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||
|
||||
if (string.IsNullOrEmpty(refreshToken))
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token not provided");
|
||||
|
||||
var token = await context.Tokens.SingleOrDefaultAsync(token => token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType);
|
||||
|
||||
if (token is null)
|
||||
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
|
||||
|
||||
if (token.CreatedAt + HopFrameAuthentication<TDbContext>.RefreshTokenTime < DateTime.Now)
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired");
|
||||
|
||||
var accessToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.AccessTokenType,
|
||||
UserId = token.UserId
|
||||
};
|
||||
|
||||
await context.Tokens.AddAsync(accessToken);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
HttpContext.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
|
||||
}
|
||||
|
||||
[HttpDelete("logout"), Authorized]
|
||||
public async Task<ActionResult> Logout() {
|
||||
var accessToken = HttpContext.User.GetAccessTokenId();
|
||||
var refreshToken = HttpContext.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||
|
||||
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
|
||||
return LogicResult.Conflict("access or refresh token not provided");
|
||||
|
||||
var tokenEntries = await context.Tokens.Where(token =>
|
||||
(token.Token == accessToken && token.Type == TokenEntry.AccessTokenType) ||
|
||||
(token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType))
|
||||
.ToArrayAsync();
|
||||
|
||||
if (tokenEntries.Length != 2)
|
||||
return LogicResult.NotFound("One or more of the provided tokens was not found");
|
||||
|
||||
context.Tokens.Remove(tokenEntries[0]);
|
||||
context.Tokens.Remove(tokenEntries[1]);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
HttpContext.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
|
||||
HttpContext.Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
||||
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
|
||||
[HttpDelete("delete"), Authorized]
|
||||
public async Task<ActionResult> Delete([FromBody] UserPasswordValidation validation) {
|
||||
var user = tokenContext.User;
|
||||
|
||||
if (!await users.CheckUserPassword(user, validation.Password))
|
||||
return LogicResult.Forbidden("The provided password is not correct");
|
||||
|
||||
await users.DeleteUser(user);
|
||||
|
||||
HttpContext.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
|
||||
HttpContext.Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
||||
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
|
||||
}
|
||||
86
src/HopFrame.Api/Extensions/MvcExtensions.cs
Normal file
86
src/HopFrame.Api/Extensions/MvcExtensions.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
//Source: https://gist.github.com/damianh/5d69be0e3004024f03b6cc876d7b0bd3
|
||||
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using IMvcCoreBuilder = Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder;
|
||||
|
||||
namespace HopFrame.Api.Extensions;
|
||||
|
||||
public static class MvcExtensions {
|
||||
/// <summary>
|
||||
/// Finds the appropriate controllers
|
||||
/// </summary>
|
||||
/// <param name="partManager">The manager for the parts</param>
|
||||
/// <param name="controllerTypes">The controller types that are allowed. </param>
|
||||
public static void UseSpecificControllers(this ApplicationPartManager partManager, params Type[] controllerTypes) {
|
||||
partManager.FeatureProviders.Add(new InternalControllerFeatureProvider());
|
||||
//partManager.ApplicationParts.Clear();
|
||||
partManager.ApplicationParts.Add(new SelectedControllersApplicationParts(controllerTypes));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only allow selected controllers
|
||||
/// </summary>
|
||||
/// <param name="mvcCoreBuilder">The builder that configures mvc core</param>
|
||||
/// <param name="controllerTypes">The controller types that are allowed. </param>
|
||||
public static IMvcCoreBuilder
|
||||
UseSpecificControllers(this IMvcCoreBuilder mvcCoreBuilder, params Type[] controllerTypes) =>
|
||||
mvcCoreBuilder.ConfigureApplicationPartManager(partManager =>
|
||||
partManager.UseSpecificControllers(controllerTypes));
|
||||
|
||||
/// <summary>
|
||||
/// Only instantiates selected controllers, not all of them. Prevents application scanning for controllers.
|
||||
/// </summary>
|
||||
private class SelectedControllersApplicationParts : ApplicationPart, IApplicationPartTypeProvider {
|
||||
public SelectedControllersApplicationParts() {
|
||||
Name = "Only allow selected controllers";
|
||||
}
|
||||
|
||||
public SelectedControllersApplicationParts(Type[] types) {
|
||||
Types = types.Select(x => x.GetTypeInfo()).ToArray();
|
||||
}
|
||||
|
||||
public override string Name { get; }
|
||||
|
||||
public IEnumerable<TypeInfo> Types { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure that internal controllers are also allowed. The default ControllerFeatureProvider hides internal controllers, but this one allows it.
|
||||
/// </summary>
|
||||
private class InternalControllerFeatureProvider : ControllerFeatureProvider {
|
||||
private const string ControllerTypeNameSuffix = "Controller";
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a given <paramref name="typeInfo"/> is a controller. The default ControllerFeatureProvider hides internal controllers, but this one allows it.
|
||||
/// </summary>
|
||||
/// <param name="typeInfo">The <see cref="TypeInfo"/> candidate.</param>
|
||||
/// <returns><code>true</code> if the type is a controller; otherwise <code>false</code>.</returns>
|
||||
protected override bool IsController(TypeInfo typeInfo) {
|
||||
if (!typeInfo.IsClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeInfo.IsAbstract) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeInfo.ContainsGenericParameters) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeInfo.IsDefined(typeof(Microsoft.AspNetCore.Mvc.NonControllerAttribute))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!typeInfo.Name.EndsWith(ControllerTypeNameSuffix, StringComparison.OrdinalIgnoreCase) &&
|
||||
!typeInfo.IsDefined(typeof(Microsoft.AspNetCore.Mvc.ControllerAttribute))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs
Normal file
20
src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using HopFrame.Api.Controller;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Security.Authentication;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HopFrame.Api.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions {
|
||||
|
||||
/// <summary>
|
||||
/// Adds all HopFrame endpoints and the HopFrame security layer to the WebApplication
|
||||
/// </summary>
|
||||
/// <param name="services">The service provider to add the services to</param>
|
||||
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
||||
public static void AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
|
||||
services.AddMvcCore().UseSpecificControllers(typeof(SecurityController<TDbContext>));
|
||||
services.AddHopFrameAuthentication<TDbContext>();
|
||||
}
|
||||
|
||||
}
|
||||
21
src/HopFrame.Api/HopFrame.Api.csproj
Normal file
21
src/HopFrame.Api/HopFrame.Api.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HopFrame.Database\HopFrame.Database.csproj" />
|
||||
<ProjectReference Include="..\HopFrame.Security\HopFrame.Security.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
21
src/HopFrame.Api/Logic/ILogicResult.cs
Normal file
21
src/HopFrame.Api/Logic/ILogicResult.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Net;
|
||||
|
||||
namespace HopFrame.Api.Logic;
|
||||
|
||||
public interface ILogicResult {
|
||||
HttpStatusCode State { get; set; }
|
||||
|
||||
string Message { get; set; }
|
||||
|
||||
bool IsSuccessful { get; }
|
||||
}
|
||||
|
||||
public interface ILogicResult<T> {
|
||||
HttpStatusCode State { get; set; }
|
||||
|
||||
T Data { get; set; }
|
||||
|
||||
string Message { get; set; }
|
||||
|
||||
bool IsSuccessful { get; }
|
||||
}
|
||||
189
src/HopFrame.Api/Logic/LogicResult.cs
Normal file
189
src/HopFrame.Api/Logic/LogicResult.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HopFrame.Api.Logic;
|
||||
|
||||
public class LogicResult : ILogicResult {
|
||||
public HttpStatusCode State { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public bool IsSuccessful => State == HttpStatusCode.OK;
|
||||
|
||||
public static LogicResult Ok() {
|
||||
return new LogicResult() {
|
||||
State = HttpStatusCode.OK
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult BadRequest() {
|
||||
return new LogicResult() {
|
||||
State = HttpStatusCode.BadRequest
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult BadRequest(string message) {
|
||||
return new LogicResult() {
|
||||
State = HttpStatusCode.BadRequest,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult Forbidden() {
|
||||
return new LogicResult() {
|
||||
State = HttpStatusCode.Forbidden
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult Forbidden(string message) {
|
||||
return new LogicResult() {
|
||||
State = HttpStatusCode.Forbidden,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult NotFound() {
|
||||
return new LogicResult() {
|
||||
State = HttpStatusCode.NotFound
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult NotFound(string message) {
|
||||
return new LogicResult() {
|
||||
State = HttpStatusCode.NotFound,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult Conflict() {
|
||||
return new LogicResult() {
|
||||
State = HttpStatusCode.Conflict
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult Conflict(string message) {
|
||||
return new LogicResult() {
|
||||
State = HttpStatusCode.Conflict,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult Forward(LogicResult result) {
|
||||
return new LogicResult() {
|
||||
State = result.State,
|
||||
Message = result.Message
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult Forward<T>(ILogicResult<T> result) {
|
||||
return new LogicResult() {
|
||||
State = result.State,
|
||||
Message = result.Message
|
||||
};
|
||||
}
|
||||
|
||||
public static implicit operator ActionResult(LogicResult v) {
|
||||
if (v.State == HttpStatusCode.OK) return new OkResult();
|
||||
|
||||
return new ObjectResult(v.Message) {
|
||||
StatusCode = (int)v.State
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class LogicResult<T> : ILogicResult<T> {
|
||||
public HttpStatusCode State { get; set; }
|
||||
|
||||
public T Data { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public bool IsSuccessful => State == HttpStatusCode.OK;
|
||||
|
||||
public static LogicResult<T> Ok() {
|
||||
return new LogicResult<T>() {
|
||||
State = HttpStatusCode.OK
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult<T> Ok(T result) {
|
||||
return new LogicResult<T>() {
|
||||
State = HttpStatusCode.OK,
|
||||
Data = result
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult<T> BadRequest() {
|
||||
return new LogicResult<T>() {
|
||||
State = HttpStatusCode.BadRequest
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult<T> BadRequest(string message) {
|
||||
return new LogicResult<T>() {
|
||||
State = HttpStatusCode.BadRequest,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult<T> Forbidden() {
|
||||
return new LogicResult<T>() {
|
||||
State = HttpStatusCode.Forbidden
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult<T> Forbidden(string message) {
|
||||
return new LogicResult<T>() {
|
||||
State = HttpStatusCode.Forbidden,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult<T> NotFound() {
|
||||
return new LogicResult<T>() {
|
||||
State = HttpStatusCode.NotFound
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult<T> NotFound(string message) {
|
||||
return new LogicResult<T>() {
|
||||
State = HttpStatusCode.NotFound,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult<T> Conflict() {
|
||||
return new LogicResult<T>() {
|
||||
State = HttpStatusCode.Conflict
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult<T> Conflict(string message) {
|
||||
return new LogicResult<T>() {
|
||||
State = HttpStatusCode.Conflict,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult<T> Forward(ILogicResult result) {
|
||||
return new LogicResult<T>() {
|
||||
State = result.State,
|
||||
Message = result.Message
|
||||
};
|
||||
}
|
||||
|
||||
public static LogicResult<T> Forward<T2>(ILogicResult<T2> result) {
|
||||
return new LogicResult<T>() {
|
||||
State = result.State,
|
||||
Message = result.Message
|
||||
};
|
||||
}
|
||||
|
||||
public static implicit operator ActionResult<T>(LogicResult<T> v) {
|
||||
if (v.State == HttpStatusCode.OK) return new OkObjectResult(v.Data);
|
||||
|
||||
return new ObjectResult(v.Message) {
|
||||
StatusCode = (int)v.State
|
||||
};
|
||||
}
|
||||
}
|
||||
13
src/HopFrame.Api/Models/SingleValueResult.cs
Normal file
13
src/HopFrame.Api/Models/SingleValueResult.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace HopFrame.Api.Models;
|
||||
|
||||
public struct SingleValueResult<TValue>(TValue value) {
|
||||
public TValue Value { get; set; } = value;
|
||||
|
||||
public static implicit operator TValue(SingleValueResult<TValue> v) {
|
||||
return v.Value;
|
||||
}
|
||||
|
||||
public static implicit operator SingleValueResult<TValue>(TValue v) {
|
||||
return new SingleValueResult<TValue>(v);
|
||||
}
|
||||
}
|
||||
5
src/HopFrame.Api/Models/UserPasswordValidation.cs
Normal file
5
src/HopFrame.Api/Models/UserPasswordValidation.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace HopFrame.Api.Models;
|
||||
|
||||
public sealed class UserPasswordValidation {
|
||||
public string Password { get; set; }
|
||||
}
|
||||
30
src/HopFrame.Api/README.md
Normal file
30
src/HopFrame.Api/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# HopFrame API module
|
||||
This module contains some useful endpoints for user login / register management.
|
||||
|
||||
## Ho to use the Web API version
|
||||
|
||||
1. Add the HopFrame.Api library to your project:
|
||||
|
||||
```
|
||||
dotnet add package HopFrame.Api
|
||||
```
|
||||
|
||||
2. Create a DbContext that inherits the ``HopDbContext`` and add a data source
|
||||
|
||||
```csharp
|
||||
public class DatabaseContext : HopDbContextBase {
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
|
||||
optionsBuilder.UseSqlite("...");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Add the DbContext and HopFrame to your services
|
||||
|
||||
```csharp
|
||||
builder.Services.AddDbContext<DatabaseContext>();
|
||||
builder.Services.AddHopFrame<DatabaseContext>();
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user