commit e0b38beff639dfe98e613d086308f5e691e33d8b Author: Carl Tibule Date: Wed Jan 25 00:06:14 2023 -0600 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3729ff0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e409bf4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,223 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Oo]bj/ +bin/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +!"DEV server.pubxml" +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Elmah Error Log Files +error-*.xml + +# MSBuild Community Tasks dll +!.build/MSBuild.Community.Tasks.dll + +# NLog Results +logs/ + +*.DotSettings + +#SpecFlow Autogenerated feature code behind files +*.feature.cs diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..ea6656e --- /dev/null +++ b/Readme.md @@ -0,0 +1,23 @@ +# YABA: Yet Another Bookmark App + +## Developer Guides + +### Running Migrations +When running migrations, .NET seems to be ignoring dependency injection settings. In order to get around this, be sure to add the connection string to the command like. For example, when adding the migration: + +``` + dotnet ef migrations add InitialMigration -p YABA.Data -s YABA.API --context YABABaseContext -- {CONNECTION_STRING_HERE} +``` + +When removing last migration: +``` + dotnet ef migrations remove InitialMigration -p YABA.Data -s YABA.API --context YABABaseContext -- {CONNECTION_STRING_HERE} +``` + +When applying migrations: +``` + dotnet ef database update -p YABA.Data -s YABA.API -c YABABaseContext -- { CONNECTION_STRING_HERE } +``` + +As per the documentation [on MSDN](https://learn.microsoft.com/en-ca/ef/core/cli/dbcontext-creation?tabs=dotnet-core-cli#from-application-services): +> The -- token directs dotnet ef to treat everything that follows as an argument and not try to parse them as options. Any extra arguments not used by dotnet ef are forwarded to the app. \ No newline at end of file diff --git a/YABA.API/Controllers/WeatherForecastController.cs b/YABA.API/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..f885e88 --- /dev/null +++ b/YABA.API/Controllers/WeatherForecastController.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace YABA.API.Controllers +{ + [ApiController] + [ApiVersion("1")] + [Authorize, Route("api/v{version:apiVersion}/[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateTime.Now.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/YABA.API/Dockerfile b/YABA.API/Dockerfile new file mode 100644 index 0000000..f06ab78 --- /dev/null +++ b/YABA.API/Dockerfile @@ -0,0 +1,22 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["YABA.API/YABA.API.csproj", "YABA.API/"] +RUN dotnet restore "YABA.API/YABA.API.csproj" +COPY . . +WORKDIR "/src/YABA.API" +RUN dotnet build "YABA.API.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "YABA.API.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "YABA.API.dll"] \ No newline at end of file diff --git a/YABA.API/Program.cs b/YABA.API/Program.cs new file mode 100644 index 0000000..6add65e --- /dev/null +++ b/YABA.API/Program.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using System.Security.Claims; +using YABA.API.Settings; +using YABA.API.Settings.Swashbuckle; +using YABA.Data.Configuration; +using YABA.Data.Context; +using YABA.Service.Configuration; + +var builder = WebApplication.CreateBuilder(args); +var configuration = builder.Configuration; + +// Add services to the container. +var auth0Section = configuration.GetSection("Authentication").GetSection("Auth0"); +var auth0Settings = auth0Section.Get(); +var domain = $"https://{auth0Settings.Domain}/"; + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}).AddJwtBearer(options => +{ + options.Authority = domain; + options.Audience = auth0Settings.Identifier; + options.TokenValidationParameters = new TokenValidationParameters + { + NameClaimType = ClaimTypes.NameIdentifier + }; +}); + +builder.Services.AddApiVersioning(setup => +{ + setup.DefaultApiVersion = new ApiVersion(1, 0); + setup.AssumeDefaultVersionWhenUnspecified = true; + setup.ReportApiVersions = true; + setup.ApiVersionReader = new UrlSegmentApiVersionReader(); +}); + +// Add services to the container. +builder.Services.AddServiceProjectDependencyInjectionConfiguration(configuration); +builder.Services.AddDataProjectDependencyInjectionConfiguration(configuration); +builder.Services.AddControllers().AddNewtonsoftJson(); + + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen( + c => + { + c.SwaggerDoc( + "v1", + new OpenApiInfo + { + Title = "YABA.API", + Version = "v1" + }); + c.OperationFilter(); + c.DocumentFilter(); + c.ResolveConflictingActions(apiDescription => apiDescription.First()); + } +); + +var app = builder.Build(); + +// Run database migrations +using (var scope = app.Services.CreateScope()) +{ + var yabaDbContext = scope.ServiceProvider.GetRequiredService(); + yabaDbContext.Database.Migrate(); +} + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/YABA.API/Properties/launchSettings.json b/YABA.API/Properties/launchSettings.json new file mode 100644 index 0000000..c266a98 --- /dev/null +++ b/YABA.API/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:7922", + "sslPort": 44310 + } + }, + "profiles": { + "YABA.API": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:7032;http://localhost:5032", + "dotnetRunMessages": true + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "publishAllPorts": true, + "useSSL": true + } + } +} \ No newline at end of file diff --git a/YABA.API/Settings/Auth0Settings.cs b/YABA.API/Settings/Auth0Settings.cs new file mode 100644 index 0000000..01c2a00 --- /dev/null +++ b/YABA.API/Settings/Auth0Settings.cs @@ -0,0 +1,10 @@ +namespace YABA.API.Settings +{ + public class Auth0Settings + { + public string Domain { get; set; } + public string ClientSecret { get; set; } + public string ClientId { get; set; } + public string Identifier { get; set; } + } +} diff --git a/YABA.API/Settings/Swashbuckle/RemoveVersionParameterFilter.cs b/YABA.API/Settings/Swashbuckle/RemoveVersionParameterFilter.cs new file mode 100644 index 0000000..47e0244 --- /dev/null +++ b/YABA.API/Settings/Swashbuckle/RemoveVersionParameterFilter.cs @@ -0,0 +1,14 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace YABA.API.Settings.Swashbuckle +{ + public class RemoveVersionParameterFilter : IOperationFilter + { + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var versionParameter = operation.Parameters.Single(p => p.Name == "version"); + operation.Parameters.Remove(versionParameter); + } + } +} diff --git a/YABA.API/Settings/Swashbuckle/ReplaceVersionWithExactValueInPathFilter.cs b/YABA.API/Settings/Swashbuckle/ReplaceVersionWithExactValueInPathFilter.cs new file mode 100644 index 0000000..824e76c --- /dev/null +++ b/YABA.API/Settings/Swashbuckle/ReplaceVersionWithExactValueInPathFilter.cs @@ -0,0 +1,18 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace YABA.API.Settings.Swashbuckle +{ + public class ReplaceVersionWithExactValueInPathFilter : IDocumentFilter + { + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + var paths = new OpenApiPaths(); + foreach (var path in swaggerDoc.Paths) + { + paths.Add(path.Key.Replace("v{version}", swaggerDoc.Info.Version), path.Value); + } + swaggerDoc.Paths = paths; + } + } +} diff --git a/YABA.API/WeatherForecast.cs b/YABA.API/WeatherForecast.cs new file mode 100644 index 0000000..59b87e7 --- /dev/null +++ b/YABA.API/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace YABA.API +{ + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} \ No newline at end of file diff --git a/YABA.API/YABA.API.csproj b/YABA.API/YABA.API.csproj new file mode 100644 index 0000000..4a4e946 --- /dev/null +++ b/YABA.API/YABA.API.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + 726eb626-1514-45b8-8521-cd7353303edb + Linux + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/YABA.API/appsettings.Development.json b/YABA.API/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/YABA.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/YABA.API/appsettings.json b/YABA.API/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/YABA.API/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/YABA.Data/Configuration/DependencyInjectionConfiguration.cs b/YABA.Data/Configuration/DependencyInjectionConfiguration.cs new file mode 100644 index 0000000..3eb286e --- /dev/null +++ b/YABA.Data/Configuration/DependencyInjectionConfiguration.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using YABA.Data.Context; + +namespace YABA.Data.Configuration +{ + public static class DependencyInjectionConfiguration + { + public static void AddDataProjectDependencyInjectionConfiguration(this IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(x => + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(configuration.GetConnectionString("YABAReadOnlyDbConnectionString")).UseSnakeCaseNamingConvention(); + return new YABAReadOnlyContext(optionsBuilder.Options); + }); + + services.AddScoped(x => { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(configuration.GetConnectionString("YABAReadWriteDbConnectionString")).UseSnakeCaseNamingConvention(); + return new YABAReadWriteContext(optionsBuilder.Options); + }); + + services.AddDbContext(options => options + .UseNpgsql(configuration.GetConnectionString("YABAReadWriteDbConnectionString")) + .UseSnakeCaseNamingConvention() + .UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll)); + + services.AddDbContext(options => options + .UseNpgsql(configuration.GetConnectionString("YABAReadWriteDbConnectionString")) + .UseSnakeCaseNamingConvention() + .UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll)); + + services.AddDbContext(options => options + .UseNpgsql(configuration.GetConnectionString("YABAReadOnlyDbConnectionString")) + .UseSnakeCaseNamingConvention() + .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)); + } + } +} diff --git a/YABA.Data/Context/YABABaseContext.cs b/YABA.Data/Context/YABABaseContext.cs new file mode 100644 index 0000000..6593d3a --- /dev/null +++ b/YABA.Data/Context/YABABaseContext.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using YABA.Models; +using YABA.Models.Interfaces; + +namespace YABA.Data.Context +{ + public class YABABaseContext : DbContext + { + public YABABaseContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Add lookup backed data here + // SAMPLE + // var lookupBackedData = Enum.GetValues(typeof(LookupEnum)).Cast(); + // modelBuilder.Entity().HasData(lookupBackedData.Select(x => new LookupModel(x))); + + + modelBuilder.Model.GetEntityTypes() + .Where(entityType => typeof(ISoftDeletable).IsAssignableFrom(entityType.ClrType)) + .ToList() + .ForEach(entityType => + { + modelBuilder.Entity(entityType.ClrType) + .HasQueryFilter(ConvertFilterExpression(e => !e.IsDeleted, entityType.ClrType)); + }); + + modelBuilder.Entity() + .HasIndex(x => new { x.BookmarkId, x.TagId }) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(x => x.Auth0Id) + .IsUnique(); + } + + public DbSet Bookmarks { get; set; } + public DbSet Tags { get; set; } + public DbSet BookmarkTags { get; set; } + public DbSet Users { get; set; } + + private static LambdaExpression ConvertFilterExpression( + Expression> filterExpression, + Type entityType) + { + // SOURCE: https://stackoverflow.com/questions/47673524/ef-core-soft-delete-with-shadow-properties-and-query-filters/48744644#48744644 + var newParam = Expression.Parameter(entityType); + var newBody = ReplacingExpressionVisitor.Replace(filterExpression.Parameters.Single(), newParam, filterExpression.Body); + + return Expression.Lambda(newBody, newParam); + } + } +} diff --git a/YABA.Data/Context/YABABaseContextFactory.cs b/YABA.Data/Context/YABABaseContextFactory.cs new file mode 100644 index 0000000..a28ea44 --- /dev/null +++ b/YABA.Data/Context/YABABaseContextFactory.cs @@ -0,0 +1,18 @@ + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace YABA.Data.Context +{ + public class YABABaseContextFactory : IDesignTimeDbContextFactory + { + public YABABaseContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(args[0]) + .UseSnakeCaseNamingConvention() + .UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll); + return new YABABaseContext(optionsBuilder.Options); + } + } +} diff --git a/YABA.Data/Context/YABAReadOnlyContext.cs b/YABA.Data/Context/YABAReadOnlyContext.cs new file mode 100644 index 0000000..d4e1db9 --- /dev/null +++ b/YABA.Data/Context/YABAReadOnlyContext.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; + +namespace YABA.Data.Context +{ + public class YABAReadOnlyContext : YABABaseContext + { + public YABAReadOnlyContext(DbContextOptions options) : base(options) + { + ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + } + } +} diff --git a/YABA.Data/Context/YABAReadWriteContext.cs b/YABA.Data/Context/YABAReadWriteContext.cs new file mode 100644 index 0000000..2867539 --- /dev/null +++ b/YABA.Data/Context/YABAReadWriteContext.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using YABA.Models.Interfaces; + +namespace YABA.Data.Context +{ + public class YABAReadWriteContext : YABABaseContext + { + public YABAReadWriteContext(DbContextOptions options) : base(options) + { + ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll; + } + + public override int SaveChanges() + { + var dateCreatedTrackableEntries = ChangeTracker + .Entries() + .Where(e => e.Entity is IDateCreatedTrackable && e.State == EntityState.Added); + + foreach (var entry in dateCreatedTrackableEntries) + ((IDateCreatedTrackable)entry.Entity).CreatedOn = DateTimeOffset.UtcNow; + + var dateModifiedTrackableItems = ChangeTracker + .Entries() + .Where(e => e.Entity is IDateModifiedTrackable && (e.State == EntityState.Modified || e.State == EntityState.Added)); + + foreach (var entry in dateModifiedTrackableItems) + ((IDateModifiedTrackable)entry.Entity).LastModified = DateTimeOffset.UtcNow; + + return base.SaveChanges(); + } + + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + var dateCreatedTrackableEntries = ChangeTracker + .Entries() + .Where(e => e.Entity is IDateCreatedTrackable && e.State == EntityState.Added); + + foreach (var entry in dateCreatedTrackableEntries) + ((IDateCreatedTrackable)entry.Entity).CreatedOn = DateTimeOffset.UtcNow; + + var dateModifiedTrackableItems = ChangeTracker + .Entries() + .Where(e => e.Entity is IDateModifiedTrackable && (e.State == EntityState.Modified || e.State == EntityState.Added)); + + foreach (var entry in dateModifiedTrackableItems) + ((IDateModifiedTrackable)entry.Entity).LastModified = DateTimeOffset.UtcNow; + + return base.SaveChangesAsync(cancellationToken); + } + } +} diff --git a/YABA.Data/Extensions/GenericDbSetExtensions.cs b/YABA.Data/Extensions/GenericDbSetExtensions.cs new file mode 100644 index 0000000..9e6427a --- /dev/null +++ b/YABA.Data/Extensions/GenericDbSetExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using System.Collections.Generic; +using System.Linq; +using YABA.Models.Interfaces; + +namespace YABA.Data.Extensions +{ + public static class GenericDbSetExtensions + { + public static void Upsert(this DbSet dbSet, T entity) where T : class, IIdentifiable + { + var entityInList = new List() { entity }; + dbSet.UpsertRange(entityInList); + } + + public static void UpsertRange(this DbSet dbSet, IEnumerable entities) where T : class, IIdentifiable + { + foreach (var entity in entities) + { + var entityExists = dbSet.Any(x => x.Id == entity.Id); + + if (entityExists) + { + EntityEntry attachedEntity = dbSet.Attach(entity); + attachedEntity.State = EntityState.Modified; + } + else + { + dbSet.Add(entity); + } + } + } + } +} diff --git a/YABA.Data/Migrations/20230125055348_InitialMigration.Designer.cs b/YABA.Data/Migrations/20230125055348_InitialMigration.Designer.cs new file mode 100644 index 0000000..da1edf3 --- /dev/null +++ b/YABA.Data/Migrations/20230125055348_InitialMigration.Designer.cs @@ -0,0 +1,221 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using YABA.Data.Context; + +namespace YABA.Data.Migrations +{ + [DbContext(typeof(YABABaseContext))] + [Migration("20230125055348_InitialMigration")] + partial class InitialMigration + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.17") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("YABA.Models.Bookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("IsHidden") + .HasColumnType("boolean") + .HasColumnName("is_hidden"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_bookmarks"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_bookmarks_user_id"); + + b.ToTable("bookmarks"); + }); + + modelBuilder.Entity("YABA.Models.BookmarkTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("BookmarkId") + .HasColumnType("integer") + .HasColumnName("bookmark_id"); + + b.Property("TagId") + .HasColumnType("integer") + .HasColumnName("tag_id"); + + b.HasKey("Id") + .HasName("pk_bookmark_tags"); + + b.HasIndex("TagId") + .HasDatabaseName("ix_bookmark_tags_tag_id"); + + b.HasIndex("BookmarkId", "TagId") + .IsUnique() + .HasDatabaseName("ix_bookmark_tags_bookmark_id_tag_id"); + + b.ToTable("bookmark_tags"); + }); + + modelBuilder.Entity("YABA.Models.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("IsHidden") + .HasColumnType("boolean") + .HasColumnName("is_hidden"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tags"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tags_user_id"); + + b.ToTable("tags"); + }); + + modelBuilder.Entity("YABA.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Auth0Id") + .IsRequired() + .HasColumnType("text") + .HasColumnName("auth0id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Auth0Id") + .IsUnique() + .HasDatabaseName("ix_users_auth0id"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("YABA.Models.Bookmark", b => + { + b.HasOne("YABA.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_bookmarks_users_user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("YABA.Models.BookmarkTag", b => + { + b.HasOne("YABA.Models.Bookmark", "Bookmark") + .WithMany() + .HasForeignKey("BookmarkId") + .HasConstraintName("fk_bookmark_tags_bookmarks_bookmark_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("YABA.Models.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .HasConstraintName("fk_bookmark_tags_tags_tag_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bookmark"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("YABA.Models.Tag", b => + { + b.HasOne("YABA.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_tags_users_user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/YABA.Data/Migrations/20230125055348_InitialMigration.cs b/YABA.Data/Migrations/20230125055348_InitialMigration.cs new file mode 100644 index 0000000..0326496 --- /dev/null +++ b/YABA.Data/Migrations/20230125055348_InitialMigration.cs @@ -0,0 +1,144 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace YABA.Data.Migrations +{ + public partial class InitialMigration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + is_deleted = table.Column(type: "boolean", nullable: false), + created_on = table.Column(type: "timestamp with time zone", nullable: false), + last_modified = table.Column(type: "timestamp with time zone", nullable: false), + auth0id = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "bookmarks", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + is_deleted = table.Column(type: "boolean", nullable: false), + created_on = table.Column(type: "timestamp with time zone", nullable: false), + last_modified = table.Column(type: "timestamp with time zone", nullable: false), + title = table.Column(type: "text", nullable: false), + description = table.Column(type: "text", nullable: false), + note = table.Column(type: "text", nullable: false), + is_hidden = table.Column(type: "boolean", nullable: false), + user_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_bookmarks", x => x.id); + table.ForeignKey( + name: "fk_bookmarks_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "tags", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + is_deleted = table.Column(type: "boolean", nullable: false), + name = table.Column(type: "text", nullable: false), + is_hidden = table.Column(type: "boolean", nullable: false), + user_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_tags", x => x.id); + table.ForeignKey( + name: "fk_tags_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "bookmark_tags", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + bookmark_id = table.Column(type: "integer", nullable: false), + tag_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_bookmark_tags", x => x.id); + table.ForeignKey( + name: "fk_bookmark_tags_bookmarks_bookmark_id", + column: x => x.bookmark_id, + principalTable: "bookmarks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_bookmark_tags_tags_tag_id", + column: x => x.tag_id, + principalTable: "tags", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_bookmark_tags_bookmark_id_tag_id", + table: "bookmark_tags", + columns: new[] { "bookmark_id", "tag_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_bookmark_tags_tag_id", + table: "bookmark_tags", + column: "tag_id"); + + migrationBuilder.CreateIndex( + name: "ix_bookmarks_user_id", + table: "bookmarks", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_tags_user_id", + table: "tags", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_users_auth0id", + table: "users", + column: "auth0id", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "bookmark_tags"); + + migrationBuilder.DropTable( + name: "bookmarks"); + + migrationBuilder.DropTable( + name: "tags"); + + migrationBuilder.DropTable( + name: "users"); + } + } +} diff --git a/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs b/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs new file mode 100644 index 0000000..0ea0f29 --- /dev/null +++ b/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs @@ -0,0 +1,219 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using YABA.Data.Context; + +namespace YABA.Data.Migrations +{ + [DbContext(typeof(YABABaseContext))] + partial class YABABaseContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.17") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("YABA.Models.Bookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("IsHidden") + .HasColumnType("boolean") + .HasColumnName("is_hidden"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_bookmarks"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_bookmarks_user_id"); + + b.ToTable("bookmarks"); + }); + + modelBuilder.Entity("YABA.Models.BookmarkTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("BookmarkId") + .HasColumnType("integer") + .HasColumnName("bookmark_id"); + + b.Property("TagId") + .HasColumnType("integer") + .HasColumnName("tag_id"); + + b.HasKey("Id") + .HasName("pk_bookmark_tags"); + + b.HasIndex("TagId") + .HasDatabaseName("ix_bookmark_tags_tag_id"); + + b.HasIndex("BookmarkId", "TagId") + .IsUnique() + .HasDatabaseName("ix_bookmark_tags_bookmark_id_tag_id"); + + b.ToTable("bookmark_tags"); + }); + + modelBuilder.Entity("YABA.Models.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("IsHidden") + .HasColumnType("boolean") + .HasColumnName("is_hidden"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tags"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tags_user_id"); + + b.ToTable("tags"); + }); + + modelBuilder.Entity("YABA.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Auth0Id") + .IsRequired() + .HasColumnType("text") + .HasColumnName("auth0id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_modified"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Auth0Id") + .IsUnique() + .HasDatabaseName("ix_users_auth0id"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("YABA.Models.Bookmark", b => + { + b.HasOne("YABA.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_bookmarks_users_user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("YABA.Models.BookmarkTag", b => + { + b.HasOne("YABA.Models.Bookmark", "Bookmark") + .WithMany() + .HasForeignKey("BookmarkId") + .HasConstraintName("fk_bookmark_tags_bookmarks_bookmark_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("YABA.Models.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .HasConstraintName("fk_bookmark_tags_tags_tag_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bookmark"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("YABA.Models.Tag", b => + { + b.HasOne("YABA.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_tags_users_user_id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/YABA.Data/YABA.Data.csproj b/YABA.Data/YABA.Data.csproj new file mode 100644 index 0000000..bff9fc3 --- /dev/null +++ b/YABA.Data/YABA.Data.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.1 + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/YABA.Models/Bookmark.cs b/YABA.Models/Bookmark.cs new file mode 100644 index 0000000..323478f --- /dev/null +++ b/YABA.Models/Bookmark.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using YABA.Models.Interfaces; + +namespace YABA.Models +{ + public class Bookmark : IIdentifiable, ISoftDeletable, IDateCreatedTrackable, IDateModifiedTrackable + { + public int Id { get; set; } + public bool IsDeleted { get; set; } + public DateTimeOffset CreatedOn { get; set; } + public DateTimeOffset LastModified { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public string Note { get; set; } + public bool IsHidden { get; set; } + + [Required] + [ForeignKey(nameof(User))] + public int UserId { get; set; } + public virtual User User { get; set; } + } +} diff --git a/YABA.Models/BookmarkTag.cs b/YABA.Models/BookmarkTag.cs new file mode 100644 index 0000000..3489c9f --- /dev/null +++ b/YABA.Models/BookmarkTag.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using YABA.Models.Interfaces; + +namespace YABA.Models +{ + public class BookmarkTag : IIdentifiable + { + public int Id { get; set; } + + [Required] + [ForeignKey(nameof(Bookmark))] + public int BookmarkId { get; set; } + public virtual Bookmark Bookmark { get; set; } + + [Required] + [ForeignKey(nameof(Tag))] + public int TagId { get; set; } + public virtual Tag Tag { get; set; } + } +} diff --git a/YABA.Models/Interfaces/IDateCreatedTrackable.cs b/YABA.Models/Interfaces/IDateCreatedTrackable.cs new file mode 100644 index 0000000..23e2452 --- /dev/null +++ b/YABA.Models/Interfaces/IDateCreatedTrackable.cs @@ -0,0 +1,9 @@ +using System; + +namespace YABA.Models.Interfaces +{ + public interface IDateCreatedTrackable + { + public DateTimeOffset CreatedOn { get; set; } + } +} diff --git a/YABA.Models/Interfaces/IDateModifiedTrackable.cs b/YABA.Models/Interfaces/IDateModifiedTrackable.cs new file mode 100644 index 0000000..fe4f7bc --- /dev/null +++ b/YABA.Models/Interfaces/IDateModifiedTrackable.cs @@ -0,0 +1,9 @@ +using System; + +namespace YABA.Models.Interfaces +{ + public interface IDateModifiedTrackable + { + public DateTimeOffset LastModified { get; set; } + } +} diff --git a/YABA.Models/Interfaces/IIdentifiable.cs b/YABA.Models/Interfaces/IIdentifiable.cs new file mode 100644 index 0000000..1129e5f --- /dev/null +++ b/YABA.Models/Interfaces/IIdentifiable.cs @@ -0,0 +1,8 @@ + +namespace YABA.Models.Interfaces +{ + public interface IIdentifiable + { + public int Id { get; set; } + } +} diff --git a/YABA.Models/Interfaces/ISoftDeletable.cs b/YABA.Models/Interfaces/ISoftDeletable.cs new file mode 100644 index 0000000..0b11b04 --- /dev/null +++ b/YABA.Models/Interfaces/ISoftDeletable.cs @@ -0,0 +1,8 @@ + +namespace YABA.Models.Interfaces +{ + public interface ISoftDeletable + { + public bool IsDeleted { get; set; } + } +} diff --git a/YABA.Models/Tag.cs b/YABA.Models/Tag.cs new file mode 100644 index 0000000..a203c24 --- /dev/null +++ b/YABA.Models/Tag.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using YABA.Models.Interfaces; + +namespace YABA.Models +{ + public class Tag : IIdentifiable, ISoftDeletable + { + public int Id { get; set; } + public bool IsDeleted { get; set; } + public string Name { get; set; } + public bool IsHidden { get; set; } + + [Required] + [ForeignKey(nameof(User))] + public int UserId { get; set; } + public virtual User User { get; set; } + } +} diff --git a/YABA.Models/User.cs b/YABA.Models/User.cs new file mode 100644 index 0000000..fd453b1 --- /dev/null +++ b/YABA.Models/User.cs @@ -0,0 +1,17 @@ +using System; +using System.ComponentModel.DataAnnotations; +using YABA.Models.Interfaces; + +namespace YABA.Models +{ + public class User : IIdentifiable, ISoftDeletable, IDateCreatedTrackable, IDateModifiedTrackable + { + public int Id { get; set; } + public bool IsDeleted { get; set; } + public DateTimeOffset CreatedOn { get; set; } + public DateTimeOffset LastModified { get; set; } + + [Required] + public string Auth0Id { get; set; } + } +} diff --git a/YABA.Models/YABA.Models.csproj b/YABA.Models/YABA.Models.csproj new file mode 100644 index 0000000..344117a --- /dev/null +++ b/YABA.Models/YABA.Models.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.1 + enable + + + + + + + diff --git a/YABA.Service/Class1.cs b/YABA.Service/Class1.cs new file mode 100644 index 0000000..955651f --- /dev/null +++ b/YABA.Service/Class1.cs @@ -0,0 +1,9 @@ +using System; + +namespace YABA.Service +{ + public class Class1 + { + + } +} diff --git a/YABA.Service/Configuration/DependencyInjectionConfiguration.cs b/YABA.Service/Configuration/DependencyInjectionConfiguration.cs new file mode 100644 index 0000000..5d88cf4 --- /dev/null +++ b/YABA.Service/Configuration/DependencyInjectionConfiguration.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace YABA.Service.Configuration +{ + public static class DependencyInjectionConfiguration + { + public static void AddServiceProjectDependencyInjectionConfiguration(this IServiceCollection services, IConfiguration configuration) + { + + } + } +} diff --git a/YABA.Service/YABA.Service.csproj b/YABA.Service/YABA.Service.csproj new file mode 100644 index 0000000..dcb837b --- /dev/null +++ b/YABA.Service/YABA.Service.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.1 + enable + + + + + + + + diff --git a/YABA.sln b/YABA.sln new file mode 100644 index 0000000..30fcfc9 --- /dev/null +++ b/YABA.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32505.173 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.API", "YABA.API\YABA.API.csproj", "{97FC2A4F-B663-43B0-987D-DC3EA7ADD66C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Models", "YABA.Models\YABA.Models.csproj", "{DDA30925-F844-426B-8B90-3E6E258BD407}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YABA.Data", "YABA.Data\YABA.Data.csproj", "{461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YABA.Service", "YABA.Service\YABA.Service.csproj", "{0098D0A8-0273-46F1-9FE7-B0409442251A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {97FC2A4F-B663-43B0-987D-DC3EA7ADD66C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97FC2A4F-B663-43B0-987D-DC3EA7ADD66C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97FC2A4F-B663-43B0-987D-DC3EA7ADD66C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97FC2A4F-B663-43B0-987D-DC3EA7ADD66C}.Release|Any CPU.Build.0 = Release|Any CPU + {DDA30925-F844-426B-8B90-3E6E258BD407}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDA30925-F844-426B-8B90-3E6E258BD407}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDA30925-F844-426B-8B90-3E6E258BD407}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDA30925-F844-426B-8B90-3E6E258BD407}.Release|Any CPU.Build.0 = Release|Any CPU + {461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5}.Release|Any CPU.Build.0 = Release|Any CPU + {0098D0A8-0273-46F1-9FE7-B0409442251A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0098D0A8-0273-46F1-9FE7-B0409442251A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0098D0A8-0273-46F1-9FE7-B0409442251A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0098D0A8-0273-46F1-9FE7-B0409442251A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8B677B7D-5CF6-460E-B4F6-7AF3BD655B12} + EndGlobalSection +EndGlobal