C
C#•3y ago
ero

ā” Creating a backend in .NET

Hi! I have absolutely zero knowledge about web development, databases, or making any kinds of web requests. Basically, I'm a complete noob when it comes to literally anything web-related (and I know databases aren't necessarily web, but you get the idea). I would like to build a backend for a website idea that's already pretty big on paper. I have a lot of ideas, have a lot of thoughts about how things should work, I just can't create it. I was wondering if there are any very beginner friendly, modern tutorials on this stuff? Preferrably videos, as I have a hard time focussing on reading a lot. If I should go into any more detail, let me know.
770 Replies
Yawnder
Yawnder•3y ago
@Ero Break your problem pieces by pieces, as you do with everything. Ignore the database aspect of it. Start with the API project template (the one that has a Weather controller endpoint) and see how you can modify it to understand. Something important for you to understand is the life cycle of an http request. How all the middleware are linked together. https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-7.0
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
What kind of frontend are you making for this? If its a JS based one like React, Vue, Svelte or whatever they are called these days, your backend will likely be a pure web API where all requests and responses are JSON (if any content at all). This matters because ASP.NET can do a lot of different things, including generating and serving up HTML (MVC, Razor pages), so you'll want to focus your attention on the aspects of it that you need.
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
TS, JS, Vue
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
Great, then a bog standard ASP.NET Web API will be your best bet. Do you know how basic HTTP stuff works, with the request/responses etc?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
i do not
Pobiega
Pobiega•3y ago
super simplified version: Every data exchange begins with the client submitting a request and the server sending a response back. You can only respond once to each request. Each request has a "method" or verb (GET, POST, PUT, DELETE, PATCH) that partially restricts what it can do but also indicates intent. You can have a webserver.com/hello endpoint respond only to GET, or GET and POST. with ASP.NET its easy to write a method that handles the get, and another method that handles the post. All requests and responses consist of several parts: the URI (path), the headers (a key value pair list) and the body (a stream, but often just JSON) when a request contains data, it can be as part of the URI, aka "query string": www.webserver.com/hello?query=floop <-- query is the parameter, floop is the value. or it can be part of the headers (they are just string-string key value pairs, so you can create them at will) or it can be the body (for apis, very common to accept a JSON object. ASP.NET has excellent support for this, you can make a record/class and just specify it as an input to your method) responses have 3 parts: the response code (indicates success or failure, and what kind of failure), headers, and optionally a body so your "endpoint" (what we call a unique path or route) can respond with 200 OK, or 404 Not Found, or 400 Bad Request, for example.
ero
eroOP•3y ago
were these just some examples or is that all of them? i thought i'd seen SET and FETCH too at some point so, what exactly is a "header"? where does it come from? i understand it's a key-value pair, but it has to be stored somewhere, no? or like, input somewhere
Esa
Esa•3y ago
It's part of the response object. a body contained inside a response object may have headers of its own in addition to the overall response object having some. Headers usually contain information such as the content-type (text/plain, application/json). These things also dictate what you can expect in return, as this is a string and "this is a string" are the result of different content-types (text/plain vs application/json) set in the Content-Type header. There may also be custom headers, some times these are prefixed by x- etc. Webservices don't usually support every http method for every endpoint, it's usually just a few if at all more than one. /api/customers/{customerId} indicates a search for a customer by a specific ID (the customerId variable) inside a collection of customers, so this one is a GET. If you remove the customerId variable it by convention becomes a POST (insert), and you'd be expected to provide the payload inside the body of your request. However that would require the webservice to have a route with a POST (as in, there needs to be an actual explicit endpoint for that path that accepts a POST httpmethod).
Pobiega
Pobiega•3y ago
It's all the ones you will likely use. There are others thou, such as HEAD, TRACE, OPTIONS, CONNECT Have not used any of them ever So yeah headers is just part of the protocol and the http client has some it adds by default, and same for the response ( added by the server) Here is a sample GET request I just googled
GET /hello.htm HTTP/1.1
User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)
Host: www.tutorialspoint.com
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
GET /hello.htm HTTP/1.1
User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)
Host: www.tutorialspoint.com
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
you wont be making these yourself quite as raw as this, as most http clients do most of the job for you for example, if you are gonna use the fetch api you literally just need the method and the URL most of the time the three most common methods are GET, POST and PUT. GET is for querying data. Sending a GET to api/customers would, if supported, return you some kind of list of customers. GET to api/customers?category=3&startsWith=steve would do similar, but filter based on the query string parameters (assuming the backend was implemented as such, but this is convention) GET to api/customers/5 would get you data about customer number 5 POST adds to collections or submits general data. POST to api/customers would register a new customer based on the body data, and return either the whole customer or just the ID (as it was assigned by the backend). PUT is "create or replace at the given id", so you could PUT to api/customers/5 to edit(replace) that customer record. PATCH is for making smaller edits than a full on replace, but its tricky to implement right. A fairly common thing to use is "jsonpatch" which has a way to indicate what to replace with what, but dont think System.Text.Json supports it just yet.
ero
eroOP•3y ago
in this fashion, would api/customers?id=5 be bad practice?
Pobiega
Pobiega•3y ago
It would not follow convention, but is technically valid The nice thing about using it as part of the URL is that you can keep going. Let's say that customers have users, you could show them at api/customers/5/users/63 etc
ero
eroOP•3y ago
yeah, this'll likely be a struggle point for me choosing what to put into the url, and what to put into the query
Pobiega
Pobiega•3y ago
Welcome to the family šŸ™‚ I tend to use url parameters for IDs and "hard" identifiers, and query strings for "filtering" multiple choices url params are obviously not optional, while query strings usually are
ero
eroOP•3y ago
i'll start setting everything up later today. i'm excited, but i'm scared i won't get far. i have a hard time learning through reading, and finding complete, modern, best practice examples is obviously impossible, because a lot of it is too personal
Pobiega
Pobiega•3y ago
Yup, very much so. Not to mention that "best pratice" changes every few weeks, šŸ˜„
ero
eroOP•3y ago
truuue
Pobiega
Pobiega•3y ago
So now that we've got most of the theory out of the way, ASP is actually really nice to work with. It handles most of the stuff for you
ero
eroOP•3y ago
i know that you use source gen for your stuff and it's like. yes. i want that. but i don't understand source generators for shit
Pobiega
Pobiega•3y ago
It has two modes thou, old-school controllers and the more modern (and highly divisive) minimal apis where endpoints are literally a single method, usually a lambda
ero
eroOP•3y ago
i believe a minimal API is all i need as well? hm, maybe not
Pobiega
Pobiega•3y ago
I think with .net 7 you can do almost everything with either approach
ero
eroOP•3y ago
the project is big. like thousands of users with a massive amount of DB data big
Pobiega
Pobiega•3y ago
minimal has better performance, but its not like controllers are terribly slow or anything it all comes down to code style preference
ero
eroOP•3y ago
(i already know this, because it's gonna be a competitor to an existing site which basically went to shit under new management)
Pobiega
Pobiega•3y ago
with controllers, you tend to make stuff like...
[Route("api/customers")]
public class CustomersController : ControllerBase
{
[HttpGet]
// no route, so it matches the "root" route of the controller
public List<Customer> GetAll() { ... }

[HttpPost]
// no route, so it matches the "root" route of the controller
public CustomerDto AddCustomer(CreateCustomerDto customer) { ... }
}
[Route("api/customers")]
public class CustomersController : ControllerBase
{
[HttpGet]
// no route, so it matches the "root" route of the controller
public List<Customer> GetAll() { ... }

[HttpPost]
// no route, so it matches the "root" route of the controller
public CustomerDto AddCustomer(CreateCustomerDto customer) { ... }
}
I actually kind of like minimal apis, but its a little bit more work getting it set up in a nice way. Just throwing api methods together in program.cs gets messy real fast (once you have hundreds of endpoints for example) so you need to add your own abstraction to separate the mapping into different files with logical responsibilities and registration of dependencies There are ofc packages that do this already, such as FastEndpoints (which I almost love, but it demands that your models are where T : new() which rules out records), and writing your own is like... 10 minutes of effort
ero
eroOP•3y ago
i mean, i don't mind that. what i just care about is doing it "properly". in such a way that makes it easy to use and especially to maintain and expand
Pobiega
Pobiega•3y ago
sure dont think there is a problem with that then if you make a boilerplate asp.net app with and without controllers you can compare them
ero
eroOP•3y ago
and also to make it as extensive as possible. like i want a very rich and intuitive api the competitor site's api is absolutely abyssmal and has zero documentation
Pobiega
Pobiega•3y ago
hm, so the API is not just for your frontends use? like, you plan to actually support external users interacting with it directly? Then you will 100% want to read up on API versioning
ero
eroOP•3y ago
yeah, it'll be a public api for fetching heaps of data don't know if you remember, but it's that leaderboards project. there will be hundreds and thousands of gaming leaderboards there will also be hundreds of "series" (a series contains multiple games (leaderboards))
Pobiega
Pobiega•3y ago
What we do at work, is we have two separate APIs more or less one for our own frontend, and one for external users
ero
eroOP•3y ago
and each leaderboard contains hundreds of submissions with potentially multiple players
Pobiega
Pobiega•3y ago
the second is heavily versioned and protected against breaking changes, while the first one we can change as we want since we use command pattern internally, most of the external stuff uses the same commands as the internal one, just with a different layer of very strict DTO mappings applied
ero
eroOP•3y ago
hm. so the entire thing (and i mean the entire thing) is gonna be open source is it safe to have the internal API open source?
Pobiega
Pobiega•3y ago
just make sure you have very good security on it šŸ˜› ie, validate absolutely everything everywhere open source means that hackers wont have to guess for exploits
ero
eroOP•3y ago
right
Pobiega
Pobiega•3y ago
but it also means you might get issues/PRs to help fix free pentesting, so to speak šŸ˜„
ero
eroOP•3y ago
i don't know whether the split between public and internal makes sense yet like if that's really necessary maybe i'll see as it moves on would you recommend going for the minimal api approach for now?
Pobiega
Pobiega•3y ago
Test it out, see how it feels. this video from nick explains the "abstraction" level I tend to use for minimal apis
Pobiega
Pobiega•3y ago
Nick Chapsas
YouTube
In defence of .NET Minimal APIs | Refactoring
Become a Patreon and get source code access: https://www.patreon.com/nickchapsas Check out my courses: https://nickchapsas.com Hello everybody I'm Nick and in this video I wanna address a lot of the critisism that Minimal APIs have been getting in the past few weeks as we are getting closer and closer to the release of .NET 6. In this video I w...
Pobiega
Pobiega•3y ago
I quite like FastEndpoints too thou, if only it would let you instantiate the response model yourself so records would be usable :((((
ero
eroOP•3y ago
You can add an empty ctor to records, no?
Pobiega
Pobiega•3y ago
Absolutely but then you can't use the short form definition
ero
eroOP•3y ago
mh, right so, where should i start?
Pobiega
Pobiega•3y ago
dotnet new webapi šŸ™‚
ero
eroOP•3y ago
hah, fair (and pretty much exactly what i wanted to hear)
Pobiega
Pobiega•3y ago
It really is that easy to get started. Asp.net is very well made and extremely modular So you start out there with like 4 lines of code and add as you go
ero
eroOP•3y ago
perhaps somewhat unrelated, but i care a lot about project structure let me run by you what i'd do
Pobiega
Pobiega•3y ago
Makes sense Project structure is important I think tebe recently made a tag for a project scaffold that looked nice. Was it... $scaffolding?
ero
eroOP•3y ago
LeaderboardsGG.Backend
ā”œā”€ .github
│ ā”œā”€ ISSUE_TEMPLATES
│ │ └─ ...
│ └─ workflows
│ └─ ...
ā”œā”€ assets
│ └─ ...
ā”œā”€ src
│ ā”œā”€ LeaderboardsGG.Backend
│ │ ā”œā”€ Properties
│ │ │ ā”œā”€ appsettings.json
│ │ │ ā”œā”€ appsettings.Development.json
│ │ │ └─ launchSettings.json
│ │ └─ LeaderboardsGG.Backend.csproj
│ └─ LeaderboardsGG.Backend.Tests
│ └─ LeaderboardsGG.Backend.Tests.csproj
ā”œā”€ .editorconfig
ā”œā”€ .gitignore
└─ LeaderboardsGG.Backend.sln
LeaderboardsGG.Backend
ā”œā”€ .github
│ ā”œā”€ ISSUE_TEMPLATES
│ │ └─ ...
│ └─ workflows
│ └─ ...
ā”œā”€ assets
│ └─ ...
ā”œā”€ src
│ ā”œā”€ LeaderboardsGG.Backend
│ │ ā”œā”€ Properties
│ │ │ ā”œā”€ appsettings.json
│ │ │ ā”œā”€ appsettings.Development.json
│ │ │ └─ launchSettings.json
│ │ └─ LeaderboardsGG.Backend.csproj
│ └─ LeaderboardsGG.Backend.Tests
│ └─ LeaderboardsGG.Backend.Tests.csproj
ā”œā”€ .editorconfig
ā”œā”€ .gitignore
└─ LeaderboardsGG.Backend.sln
is it possible at all to put the appsettings.json into the Properties folder? i don't like having it in the root of the project
Pobiega
Pobiega•3y ago
Possible, but would require changing the default web host builder a bit Ie, not using the default :p
ero
eroOP•3y ago
fine by me shouldn't be more than 2 lines right?
Pobiega
Pobiega•3y ago
Eh, probably closer to 10 But still doable
Accord
Accord•3y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
ero
eroOP•3y ago
@Pobiega so the minimal api stuff is pretty clean, but i really don't like how it's set up. it just feels kinda... weird to split everything apart like that. because of that, i'd like to look into source gen. do you think something like this would make sense?
[HttpRepo] // custom attribute to generate .MapGet, .MapPost, etc.
public partial class CustomerRepository
{
[HttpModel] // custom attribute to identify the repo's model
public record Customer(Guid Id, string FullName);

private readonly Dictionary<Guid, Customer> _customers = new();

[HttpGet("/customers")] // not used for anything besides identifying what to generate
public List<Customer> GetAll()
{
return _customers.Values.ToList();
}
}
[HttpRepo] // custom attribute to generate .MapGet, .MapPost, etc.
public partial class CustomerRepository
{
[HttpModel] // custom attribute to identify the repo's model
public record Customer(Guid Id, string FullName);

private readonly Dictionary<Guid, Customer> _customers = new();

[HttpGet("/customers")] // not used for anything besides identifying what to generate
public List<Customer> GetAll()
{
return _customers.Values.ToList();
}
}
Pobiega
Pobiega•3y ago
Could work for sure. if you just want to generate CRUD stuff for you, that'll do it Might get complicated when you try and add validation of inputs and stuff thou
ero
eroOP•3y ago
the thing is, i don't know if i need anything more in the past, i've only experienced the entity + dto + controller + service way of making a web api which feels a lot more complete
Pobiega
Pobiega•3y ago
minimal apis only really replace the controller
ero
eroOP•3y ago
and the entity? or the dto i guess?
Pobiega
Pobiega•3y ago
you still need those. and DTOs too
ero
eroOP•3y ago
but isn't the entity the dto in minimal apis? like aren't they the same thing in this? Customer is both the entity and the dto, no?
Pobiega
Pobiega•3y ago
in this case yes but that comes with issues what if you want to add some calculated properties to the response object, but not to your database entity? or if you need to add some hidden fields to the entity that should not be shown to the user?
ero
eroOP•3y ago
this could be handled by that HttpModel attribute too i guess
Pobiega
Pobiega•3y ago
or if you have multiple endpoints using the same entity with different information, like an admin vs a user endpoint
ero
eroOP•3y ago
i feel like minimal apis just aren't it
Pobiega
Pobiega•3y ago
Controllers are still a thing so you can absolutely just use them if you prefer that
ero
eroOP•3y ago
mh. is there like anything to get me started on this?
Pobiega
Pobiega•3y ago
the source gen stuff? or just the controllers?
ero
eroOP•3y ago
just, everything else i don't know how to explain it. like i don't know anything about anything web dev related i don't even really know what entities and dtos and controllers and services are i don't know how they work together
Pobiega
Pobiega•3y ago
right
ero
eroOP•3y ago
i don't know what they do
Pobiega
Pobiega•3y ago
well lets go though that then, might clear some things up
ero
eroOP•3y ago
i don't know how to test things, how to run things to test them
Pobiega
Pobiega•3y ago
Entity is your core domain object. its the class that represents the true state of an item you care about its usually saved in your database
ero
eroOP•3y ago
what's a domain object
Pobiega
Pobiega•3y ago
this project, you said its about game leaderboards right?
ero
eroOP•3y ago
yup i mean i know what my entities are right like User, Leaderboard, Submission
Pobiega
Pobiega•3y ago
yeah exactly those are your entities. its the stuff you save in your database and your core logic work with
ero
eroOP•3y ago
ok wait wait i wanna write code on the side so i can remember this and since i still care about project structure; do you think entities should be in their own namespace X.Entities?
Pobiega
Pobiega•3y ago
usually just x.Domain a classlib that contains your entities and little else.
ero
eroOP•3y ago
really an entire project just for the entities
Pobiega
Pobiega•3y ago
its very common, yes. the reason being what if you want to reuse those entities for a cli tool, or a WPF admin app, or something but you can ofc just put it in a namespace somewhere
ero
eroOP•3y ago
would you call them UserEntity or just User?
Pobiega
Pobiega•3y ago
User
ero
eroOP•3y ago
would they be records or classes? i believe you've said you use records for everything
Pobiega
Pobiega•3y ago
depends on what database ORM you use EF for example doesnt like record entities if you use dapper, I think its fine
ero
eroOP•3y ago
still? in net7?
Pobiega
Pobiega•3y ago
well, EFs primary thing is the change tracker if your entites are immutable... no changes to track :p
ero
eroOP•3y ago
right class it is so, guid or ulong for ids?
Pobiega
Pobiega•3y ago
I like guids. prevents object enumeration, which is a security flaw
ero
eroOP•3y ago
but i do want users to be enumerated i guess not the entities in particular
Pobiega
Pobiega•3y ago
in what way? ie, if you see the url: api/user/500 you can try to access api/user/501 too and if my code sucks, perhaps you get in or see some data you shouldnt or even NOT get a 404 which tells you there is a user with that ID
ero
eroOP•3y ago
why's that a problem? i want anyone to be able to GET a user by their id of course the information they get back is limited and if someone wants to GET all users on the site, then go right ahead
Pobiega
Pobiega•3y ago
alright, if thats a desired trait then go ahead and use ulongs
ero
eroOP•3y ago
so, about usernames. the original idea was to have it be [a-zA-Z0-9_'-]{2,32} but perhaps i don't want usernames like that maybe i want a system like discord
Pobiega
Pobiega•3y ago
Shouldn't be much of a problem.
ero
eroOP•3y ago
also i've heard that ulong is a problem, but long less so is there any merit to that?
Pobiega
Pobiega•3y ago
Not sure why ulong would be a problem, but long/int are very common as ID types, even if you never really expect negative ids
ero
eroOP•3y ago
also by this i mean that _'-, if used at all, must be surrounded by alphanum characters so __ or a- don't work for usernames
Pobiega
Pobiega•3y ago
right. Thats all up to you, not a problem either way
ero
eroOP•3y ago
but also then the user page link will have to contain their id, which is ugly
Pobiega
Pobiega•3y ago
if you want a discord like system, there are two unique identifiers for a user right the ID, and the "name#number"
ero
eroOP•3y ago
right
Pobiega
Pobiega•3y ago
I believe the discord API term for the id is the snowflake
ero
eroOP•3y ago
yup
Pobiega
Pobiega•3y ago
yeah, not very pretty, but also primarily used when its important it cant be changed like in a ban list for a user endpoint, api/users/Ero#1111 would be fine
ero
eroOP•3y ago
is that a valid url? didn't know if you could include the hash
Pobiega
Pobiega•3y ago
hm right, hash is used for html anchors so probably not
ero
eroOP•3y ago
would it be very difficult to change from usernames to tags later on? i imagine so hm, unless
Pobiega
Pobiega•3y ago
like going from just ID + name, to ID + tag + name?
ero
eroOP•3y ago
yeah
Pobiega
Pobiega•3y ago
probably not, as long as you used the ID for all relationships
ero
eroOP•3y ago
right i'll go with normal usernames then for now
namespace Lbgg.Domain;

public sealed class User
{
[Required]
public required ulong Id { get; set; }

[Required]
[MinLength(2)]
[MaxLength(32)]
[RegularExpression(@"^[a-zA-Z0-9][a-zA-Z0-9_'-]*[a-zA-Z0-9]$")]
public required string Name { get; set; }
}
namespace Lbgg.Domain;

public sealed class User
{
[Required]
public required ulong Id { get; set; }

[Required]
[MinLength(2)]
[MaxLength(32)]
[RegularExpression(@"^[a-zA-Z0-9][a-zA-Z0-9_'-]*[a-zA-Z0-9]$")]
public required string Name { get; set; }
}
is this acceptable? should i not be using those attributes? also i just wanna thank you a bunch for doing this with me, i'd be absolutely lost otherwise
Pobiega
Pobiega•3y ago
I personally dislike attribute validation I prefer something like FluentValidation instead but thats preference my reason being that its not clear WHEN attribute validation takes place
ero
eroOP•3y ago
that's understandable i'll leave the attributes out for now
public sealed class User
{
public required ulong Id { get; set; }
public required string Name { get; set; }
public required string Email { get; set; }
public required string Password { get; set; }

public string? About { get; set; }
}
public sealed class User
{
public required ulong Id { get; set; }
public required string Name { get; set; }
public required string Email { get; set; }
public required string Password { get; set; }

public string? About { get; set; }
}
so we're here now
Pobiega
Pobiega•3y ago
seems good!
ero
eroOP•3y ago
things subject to change of course
Pobiega
Pobiega•3y ago
ofc
ero
eroOP•3y ago
and you just have this in the root of the .Domain project? not like .Domain.Entities or something?
Pobiega
Pobiega•3y ago
probably something like the latter, as I tend to eventually add more things to Domain
ero
eroOP•3y ago
sounds good what'd be my next step? i guess at this point i have to decide whether i want a minimal api or not, huh
Pobiega
Pobiega•3y ago
yup, next step is figuring out what endpoints you want so that means deciding on minimal vs controller
ero
eroOP•3y ago
so, i want my actual user pages to be /u/{username} does that have anything to do with anything? or is that the frontend's job
Pobiega
Pobiega•3y ago
primarily frontend, but you'll need to consider the data the frontend has if thats the route you want, you'll need an API endpoint that takes in a username and returns a user object
ero
eroOP•3y ago
this would make the switch from usernames to tags more difficult i think
Pobiega
Pobiega•3y ago
indeed it would or well would it?
ero
eroOP•3y ago
well, not for me
Pobiega
Pobiega•3y ago
your endpoint would swap to be /u/tag
ero
eroOP•3y ago
but it'd result in a lot of broken links like if someone has their user page linked somewhere
Pobiega
Pobiega•3y ago
right, true
ero
eroOP•3y ago
argh, really struggling over this using the ID isn't particularly weird, lots of sites do it but i find it ugly and using the username directly is a lot more intuitive and leads to more readable links
Pobiega
Pobiega•3y ago
well, then go with tags from the start? if its a feature you know you'll want later on
ero
eroOP•3y ago
i'd just gonna go with IDs in the url i think because people can change their tags too right
Pobiega
Pobiega•3y ago
šŸ‘
ero
eroOP•3y ago
that's the whole point
Pobiega
Pobiega•3y ago
yep so you'd get broken links anyways
ero
eroOP•3y ago
and if they change their tag, or their numbers, links broken again alright do you have any suggestions on what my next step could be?
Pobiega
Pobiega•3y ago
Controllers vs minimal apis. Controllers gives you a natural way to group similar endpoints together, but also comes with a little bloat in that every action (endpoint) will have all the dependencies resolved for it so even if only 1 out of 5 endpoints in your controller require the database connection, it will be resolved for all of them (since the controller is the item being resolved)
ero
eroOP•3y ago
does any of this have to do with the speed of the requests? i care a lot about speed
Pobiega
Pobiega•3y ago
it does. last time I saw numbers, I think it was 11-15% faster with minimal apis
ero
eroOP•3y ago
oh boy
Pobiega
Pobiega•3y ago
granted, you're still in the "handles 10000 requests per second" bracket without even trying
ero
eroOP•3y ago
oh lol yeah i doubt that's ever gonna happen maybe at peak times i mean i like the source gen approach the one i came up with seemed nice and tidy to me
Pobiega
Pobiega•3y ago
sounds neat, just worried it might be hard to implement nicely
ero
eroOP•3y ago
yeah...
Pobiega
Pobiega•3y ago
for example an UpdateUser endpoint might not take in the same DTO as a CreateUser
ero
eroOP•3y ago
mh
Pobiega
Pobiega•3y ago
where do you want your business logic to live btw? You normally have two options: service classes or command pattern actually its a very similar decision as controller vs minimal api
ero
eroOP•3y ago
i've been wanting to use commands ever since i saw someone (i believe it was you?) use source gen with them as well
Pobiega
Pobiega•3y ago
hmm Don't remember using source gen with them, but I know you can ie, use a generated dispatcher so there is not even startup cost to initializing it haha, found this while googling: https://github.com/Burgyn/MMLib.MediatR.Generators it generates HTTP endpoints based on your commands ah here it is: https://github.com/martinothamar/Mediator source generated command/message dĆ­spatcher
ero
eroOP•3y ago
namespace Lbgg.Controllers.Users;

[HttpCommand]
public static partial class Create
{
public sealed partial record Command(
ulong Id,
string Name)
{
private static void AddValidation(AbstractValidator<Command> v)
{
v.RuleFor(u => u.Name).NotEmpty().MinimumLength(2).MaximumLength(32);
}
}

private static async Task CommandHandler(
Command command,
ApplicationDbContext dbContext,
ILogger logger)
{
if (await context.Users.AnyAsync(u => u.Id == command.Id))
throw new InvalidOperationException($"User '{command.Id}' already exists.");

var user = new UserEntity
{
UserName = command.UserName,
Id = command.Id,
};

context.Users.Add(user);
await context.SaveChangesAsync();

using (logger.AddProperties(("@NewUserInformation", command)))
logger.LogInformation("New user created");
}
}
namespace Lbgg.Controllers.Users;

[HttpCommand]
public static partial class Create
{
public sealed partial record Command(
ulong Id,
string Name)
{
private static void AddValidation(AbstractValidator<Command> v)
{
v.RuleFor(u => u.Name).NotEmpty().MinimumLength(2).MaximumLength(32);
}
}

private static async Task CommandHandler(
Command command,
ApplicationDbContext dbContext,
ILogger logger)
{
if (await context.Users.AnyAsync(u => u.Id == command.Id))
throw new InvalidOperationException($"User '{command.Id}' already exists.");

var user = new UserEntity
{
UserName = command.UserName,
Id = command.Id,
};

context.Users.Add(user);
await context.SaveChangesAsync();

using (logger.AddProperties(("@NewUserInformation", command)))
logger.LogInformation("New user created");
}
}
this is what someone has posted in the past i saw this and liked it a lot and then you have
public sealed partial class UserController
{
[HttpPost]
public async Task Create(Create.Command command)
{
return await _sender.Send(command);
}
}
public sealed partial class UserController
{
[HttpPost]
public async Task Create(Create.Command command)
{
return await _sender.Send(command);
}
}
Pobiega
Pobiega•3y ago
Hm, thats neat. And it would generate the Http endpoint I assume
ero
eroOP•3y ago
or something so the Create.Command record would be my Create dto i guess
Pobiega
Pobiega•3y ago
exactly.
ero
eroOP•3y ago
i don't really know how to approach that yet, even in the slightest so let's just try to write it without any source gen in mind yet
Pobiega
Pobiega•3y ago
alright
ero
eroOP•3y ago
just the full thing minimal api, commands
Pobiega
Pobiega•3y ago
šŸ‘
ero
eroOP•3y ago
so first about namespaces. how should i split things up, what do i put where Nick had Repositories, Models and EndpointDefinitions this seems like so much
Pobiega
Pobiega•3y ago
There are a bunch of different approaches for sure I like to group mine by ... "area", if that makes sense like, lets say I have a few user endpoints
ero
eroOP•3y ago
besides me not even knowing what a repository is in this context
Pobiega
Pobiega•3y ago
I'd probably stick them in X.Web.Endpoints.Users and that would contain the endpoints themselves, the DTOs, etc possible as subnamespaces
ero
eroOP•3y ago
so let's say, something like this?
namespace Lbgg.Endpoints.Dtos;

public sealed record CreateUser(
string Name,
string Email,
string Password);
namespace Lbgg.Endpoints.Dtos;

public sealed record CreateUser(
string Name,
string Email,
string Password);
Pobiega
Pobiega•3y ago
yup well I'd scope it down a bit more the CreateUser dto is only ever relevant within the CreateUser endpoint really
ero
eroOP•3y ago
oh, that was supposed to be in Lbgg.Endpoints.Users.Dtos
Pobiega
Pobiega•3y ago
right then that is perfectly fine there is an interesting idea someone raise a while back, which is just straight up using your commands as DTOs its doable, but comes with the problem that changing your command at all will likely be a breaking API change
ero
eroOP•3y ago
i wonder...
[HttpEndpointGenerator]
public static class UserEndpoints
{
// generates the dto, the controller, and whatever else is needed
public static async Task Create(string name, string email, string password)
{
// create user
}
}
[HttpEndpointGenerator]
public static class UserEndpoints
{
// generates the dto, the controller, and whatever else is needed
public static async Task Create(string name, string email, string password)
{
// create user
}
}
how fun would this be it just like generates
namespace Lbgg.Endpoints.Users;

file static class Create
{
record Command(string Name, string Email, string Password);

static async Task Execute(Command c) => UserEndpoints.Create(c.Name, c.Email, c.Password);
}
namespace Lbgg.Endpoints.Users;

file static class Create
{
record Command(string Name, string Email, string Password);

static async Task Execute(Command c) => UserEndpoints.Create(c.Name, c.Email, c.Password);
}
or something but i mean, i don't even know what you actually need to generate to make it work swagger integration is a pretty important to me too i want very good api documentation
Pobiega
Pobiega•3y ago
Not a problem, thats available with both minimal APIs and controllers
Pobiega
Pobiega•3y ago
I'll throw in another recommendation for https://fast-endpoints.com/
FastEndpoints
FastEndpoints
FastEndpoints is a developer friendly alternative to Minimal APIs & MVC for rapid REST API development.
Pobiega
Pobiega•3y ago
like I said, my only gripe with this is that it creates the response instance for you by default, which ruins records for me Should doublecheck if thats still a thing, doesnt seem like it is by a quick glance at the code
ero
eroOP•3y ago
i mean, yeah, right, that's cool and all, but i don't relaly understand what any of that does or means. like i'm still very much lost on how to actually create an api. what services are, what controllers are, what endpoints really are i don't know how to explain it. like i feel like i'm in the middle of the ocean with no support everything is going on at once and i don't know where to go
Pobiega
Pobiega•3y ago
well lets just start out easy then an endpoint is just some piece of code that is registered to handle requests to a given URL for example, POST api/users could be the route for an endpoint that adds users
ero
eroOP•3y ago
probably a language thing but the words chosen for these things are very weird to me requests endpoints so what i wanna do right now is just, get to the point where i can run my api, open it in swagger, and add stuff in my in-memory db i think once that all works, i can expand on that a bit more easily
Pobiega
Pobiega•3y ago
Sure Lets start out with controllers for now then, as thats the easiest way to move forward, imho I guess you have an asp.net webapi project made already?
ero
eroOP•3y ago
yeah i deleted everything though like the stupid ass weather forecast stuff
Pobiega
Pobiega•3y ago
not a problem what does your program.cs look like?
ero
eroOP•3y ago




man can't display empty blocks it's empty
Pobiega
Pobiega•3y ago
okay, lets add the basics back in then
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();
this will add all controllers to your DI container, add swagger, then build the app (looks at configurations etc), then configure the HTTP pipeline by adding swagger if we are in development mode, adding HTTPS redirection, adding support for authorization and finally mapping your controllers this should compile and start up a fairly empty swagger UI on boot
ero
eroOP•3y ago
lots of things there that i still don't know
Pobiega
Pobiega•3y ago
lets just leave it like this for now so we can get to the point where your API can recieve a request and do something with it
ero
eroOP•3y ago
right
ero
eroOP•3y ago
Pobiega
Pobiega•3y ago
development cert for HTTPS
ero
eroOP•3y ago
oh yeah i wanna use postgres
Pobiega
Pobiega•3y ago
so you dont get warnings that localhost doesnt have a valid trusted HTTPS cert
ero
eroOP•3y ago
i guess that doesn't have anything to do with the web part actually
Pobiega
Pobiega•3y ago
nope, thats fine npgsql has excellent connectors for C# and EF, or dapper or whatever you wanna use
ero
eroOP•3y ago
ok yeah that works
ero
eroOP•3y ago
Pobiega
Pobiega•3y ago
cool lets add our first controller make a new class anywhere in the web project, something like UsersController make it inherit ControllerBase, and decorate it with [ApiController] also, give it a [Route("api/users")] attribute you can ofc change that string to be whatever base URI you want for this controller finally, lets make a simple method:
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
[HttpGet("{id}")]
public User GetUser(ulong id)
{
return new User()
{
Id = id,
About = "not yet",
Email = "[email protected]",
Name = "Steve",
Password = "oh dear god"
};
}
}
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
[HttpGet("{id}")]
public User GetUser(ulong id)
{
return new User()
{
Id = id,
About = "not yet",
Email = "[email protected]",
Name = "Steve",
Password = "oh dear god"
};
}
}
now, this is obviously a stupid method, but it should work and whatever ID you give as a parameter should be present in the json response controllers are added to the DI container, so you can just add a constructor with whatever dependencies you want access to, and they will be injected when the controller is resolved for example, a database context
ero
eroOP•3y ago
i don't really know what DI or DI containers are
Pobiega
Pobiega•3y ago
Hm, okay. DI is a common way to solve the IoC problem (inversion of control) like, we want access to the database inside our controller, so we can query a user from the database but we dont want the controller to be responsible for creating connections to the database directly via something like var db = new DatabaseConnection();
ero
eroOP•3y ago
i guess i just don't really understand what we're injecting?
Pobiega
Pobiega•3y ago
instead, we say that the controller gets a constructor parameter for the database connection
ero
eroOP•3y ago
what dependency are we injecting
Pobiega
Pobiega•3y ago
the database connection.
ero
eroOP•3y ago
to where
Pobiega
Pobiega•3y ago
for once we have one to the controller, in this case.
ero
eroOP•3y ago
where are we injecting it i'm so confused we're not doing anything though
Pobiega
Pobiega•3y ago
well currently we are not injecting anything either since the method just returns dummy data each time lets play around with this a bit more and worry about the database stuff later
ero
eroOP•3y ago
alright swagger ain't too smart, huh?
Pobiega
Pobiega•3y ago
hm?
ero
eroOP•3y ago
Pobiega
Pobiega•3y ago
Pobiega
Pobiega•3y ago
this is what I get.
ero
eroOP•3y ago
should be uint64
Pobiega
Pobiega•3y ago
hm, yeah it should. curious that it didnt figure that out tbh
ero
eroOP•3y ago
and yet it knows that -123 is invalid
ero
eroOP•3y ago
Pobiega
Pobiega•3y ago
Pobiega
Pobiega•3y ago
OpenAPI Specification - Version 3.0.3 | Swagger
The OpenAPI Specification defines a standard interface to RESTful APIs which allows both humans and computers to understand service capabilities without access to source code, documentation, or network traffic inspection.
Pobiega
Pobiega•3y ago
no unsigned types in that specification bit silly
ero
eroOP•3y ago
i guess i might just go with long then?
Pobiega
Pobiega•3y ago
Sure, thats fine but hey, we have a working endpoint that responds as expected
ero
eroOP•3y ago
yup
Pobiega
Pobiega•3y ago
whats the next step? making it actually remember something?
ero
eroOP•3y ago
i suppose, yeah
Pobiega
Pobiega•3y ago
alright, the simplest "in memory database" is just a class with lists in it šŸ™‚ so we can make one of those for now
public class Database
{
public List<User> Users { get; } = new();
}
public class Database
{
public List<User> Users { get; } = new();
}
there we go, almost a database Now, we want to get this into our controller so we can start using it there but we just var database = new Database(); it wont work, as we'll get a new database on each request and lets not use static either, since we want this to simulate being a real database connection, which we cant have static
ero
eroOP•3y ago
why not?
Pobiega
Pobiega•3y ago
what part? not being static?
ero
eroOP•3y ago
yeah the static part not like i'm looking for reasons to use static i just wanna know why
Pobiega
Pobiega•3y ago
has to do with object lifetimes etc the connection itself is disposable I'm sort of assuming you will be using EF at some point for now but I think this holds true for dapper or similar too
ero
eroOP•3y ago
EF was the idea yeah
Pobiega
Pobiega•3y ago
cool then we'll go with that so, to get access to the database, we need to inject it add a constructor to your controller:
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
private readonly Database _database;

public UsersController(Database database)
{
_database = database;
}
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
private readonly Database _database;

public UsersController(Database database)
{
_database = database;
}
it takes in a database and just saves the reference in a readonly field. this is very standard now if we run this, we get an error because we never told the DI container what a Database is so back to program.cs we go and... builder.Services.AddSingleton<Database>(); there are 3 methods of note on Services here (which is an IServiceCollection, btw) AddSingleton, AddScoped and AddTransient singelton just means "there will only ever be one instance of this" scoped means "there will only ever be one instance of this, PER SCOPE" (in ASP.NET thats usually request scope, meaning a single http request transient means "you'll get a new one each time"
ero
eroOP•3y ago
alright
Pobiega
Pobiega•3y ago
so since we want our database to live as long as the application itself, we say its singleton now we can use it in our controller
ero
eroOP•3y ago
oh sick
Pobiega
Pobiega•3y ago
so a logical rewrite of our get user method would be...
[HttpGet("{id}")]
public ActionResult<User> GetUser(long id)
{
var user = _database.Users.FirstOrDefault(x => x.Id == id);

if (user is null)
{
return NotFound();
}

return user;
}
[HttpGet("{id}")]
public ActionResult<User> GetUser(long id)
{
var user = _database.Users.FirstOrDefault(x => x.Id == id);

if (user is null)
{
return NotFound();
}

return user;
}
we look in the database. nothing found? well we want to return a 404 NOT FOUND http response controllers have helper methods for that. But we need to change the return type a bit ActionResult<T> lets us be strictly typed but return whatever http response codes we want
ero
eroOP•3y ago
makes sense
Pobiega
Pobiega•3y ago
okay cool. lets try making the "CreateUser" endpoint now wanna give it a go?
ero
eroOP•3y ago
sure thing
public record CreateUserDto(
string Name,
string Email,
string Password);


public ActionResult CreateUser(CreateUserDto userToCreate)
{
if (_database.Users.Any(u => u.Name == userToCreate.Name))
{
return Conflict();
}

_database.Users.Add(new()
{
Id = /* ? */,
Name = userToCreate.Name,
Email = userToCreate.Email,
Password = userToCreate.Password
});

return Success(); // The name 'Success' does not exist in the current context
}
public record CreateUserDto(
string Name,
string Email,
string Password);


public ActionResult CreateUser(CreateUserDto userToCreate)
{
if (_database.Users.Any(u => u.Name == userToCreate.Name))
{
return Conflict();
}

_database.Users.Add(new()
{
Id = /* ? */,
Name = userToCreate.Name,
Email = userToCreate.Email,
Password = userToCreate.Password
});

return Success(); // The name 'Success' does not exist in the current context
}
hm
Pobiega
Pobiega•3y ago
yeah, pretty much! normally we'd get the next available ID from the database itself, but our memory collection doesnt support that so we can add a simple tracker if we want, but idk maybe thats overkill šŸ˜›
ero
eroOP•3y ago
but what do i return? there's no Success() and no ActionResult.Success oh it's Ok() why are they even methods pisses me off
Pobiega
Pobiega•3y ago
Well, because they often take arguments
ero
eroOP•3y ago
ah, right is there a clear "next step"?
Pobiega
Pobiega•3y ago
I'd say introduce command pattern so instead of the controllers doing actual logic, they'd only handle HTTP stuff next step is probably adding an actual database context via EF, so you can start persisting stuff Would highly recommend reading up/watching some videos on dependency injection and EF
ero
eroOP•3y ago
let's do this first and i'll worry about the rest after
Pobiega
Pobiega•3y ago
Okay. https://github.com/martinothamar/Mediator seems good, so you can use that if you don't want to go through the hassle of writing your own dispatcher
ero
eroOP•3y ago
i do wanna go through the hassle for now so i at least understand
Pobiega
Pobiega•3y ago
Okay, so the simplest version of the command pattern with DI is to register your handlers with AddScoped (most will need to be scoped, because the database context will be scoped), and simply inject them into your controller pros: no dispatcher, yay cons: a controller might use many handlers, but perhaps only one at a time
ero
eroOP•3y ago
so i really didn't understand a lot of that what's a dispatcher what's a handler in this context
Pobiega
Pobiega•3y ago
Handler = command handler a command is just a POCO, a stupid no-logic C# class/record that only guarantees that its "valid" (ie, if it needs an ID that an ID was provided) a dispatcher lets you use the fancy calls, like...
var command = new CreateUserCommand(...);
var result = _dispatcher.Run(command);
var command = new CreateUserCommand(...);
var result = _dispatcher.Run(command);
and result here would be your User, or whatever the handler for CreateUser responds with the type inference here is based on the fact that the command is generic over its result type, btw so a dispatch-less alterantive is
var command = new CreateUserCommand(...);
var result = _createUserHandler.Handle(command);
var command = new CreateUserCommand(...);
var result = _createUserHandler.Handle(command);
but as said, that means instead of just injecting the dispatcher, you'll need to know what handlers you need
ero
eroOP•3y ago
i was wondering more about the actual implementation, not just how it'll be used in the end
Pobiega
Pobiega•3y ago
Well, they are non trivial and I really CBA making one from scratch right now :p But the idea is that you assembly scan for your handlers at startup (or sourcegen) And build a lookup of command type to handler type
ero
eroOP•3y ago
ah, that was part of the nick video yeah he does assembly scanning there
Pobiega
Pobiega•3y ago
You can then resolve the handlers based on the command type
ero
eroOP•3y ago
surprisingly less lines than i expected
Pobiega
Pobiega•3y ago
Yeah the reflection stuff isnt too bad. In general the very basics isn't too much code in general, but it adds up
ero
eroOP•3y ago
need to wait for patrick to go back to bed to ask my source gen question i guess
Patrick
Patrick•3y ago
you realise im asking those questions to make you realise it's backward
ero
eroOP•3y ago
won't leave me alone
Patrick
Patrick•3y ago
because making remarks about someone behind their back... is great?
Patrick
Patrick•3y ago
if you can answer the question it'll help you build a better backend, considering im reading this now and you've never done it before
ero
eroOP•3y ago
i can't answer the question i still don't even really know what a controller or a service or mediatr or whatever are i just know that what i have been doing so far is fucking annoying
Patrick
Patrick•3y ago
so your protest to me asking questions has actually uncovered you don't know what you're doing here's a simple scenario: write a controller for what you want to do and leave it there
ero
eroOP•3y ago
i don't know what i want to do there is so much this site needs to do i don't know where to start
Patrick
Patrick•3y ago
im not sure if this has become a question of how to structure your API or how to start a project im now supposing the latter
ero
eroOP•3y ago
both
Patrick
Patrick•3y ago
ok so what is it
ero
eroOP•3y ago
what's what
Patrick
Patrick•3y ago
your project
ero
eroOP•3y ago
gaming leaderboards for score and time
Patrick
Patrick•3y ago
ok and are you going to let people add/manage those?
ero
eroOP•3y ago
indeed
Patrick
Patrick•3y ago
ok so we already have 2 domain objects, Leaderboard and User are you going to store users yourself or use a third party i.e. OAuth/etc
ero
eroOP•3y ago
i don't know what that means
Patrick
Patrick•3y ago
are you going to have a sign up page where users put in their email and a password, or are you going to shell out authentication to Discord/GitHub/Facebook/etc
ero
eroOP•3y ago
there's also like dozens of objects more both...? i guess?
Patrick
Patrick•3y ago
sure, im starting from scratch to look at requirements, because you said you don't know where to start ok so immediately you're going to need to build from the ground up a user controller which can handle the sign up so a request to the API would look like POST /api/user which adds a new user, so we'll have a UserController.
ero
eroOP•3y ago
like i'd like to make sign up as easy as possible and would like to support at least google and github
Patrick
Patrick•3y ago
is this an MVC site?
ero
eroOP•3y ago
but also give the option not to use either of course it's not a site at all just the api frontend is TS and Vue
Patrick
Patrick•3y ago
ok well you'll need a controller to handle manual sign up
[ApiController]
public class UserController : ControllerBase
{
private readonly EntityContext _db;

public UserController(EntityContext db) => _db = db;

[HttpPost]
public async Task<IActionResult> Add([FromBody] UserDto user)
{
var userEntity = new User
{
Username = user.Username,
Password = HashPassword(user.Password)
};

_db.Add(userEntity);
await _db.SaveChangesAsync();
return Ok();
}
}
[ApiController]
public class UserController : ControllerBase
{
private readonly EntityContext _db;

public UserController(EntityContext db) => _db = db;

[HttpPost]
public async Task<IActionResult> Add([FromBody] UserDto user)
{
var userEntity = new User
{
Username = user.Username,
Password = HashPassword(user.Password)
};

_db.Add(userEntity);
await _db.SaveChangesAsync();
return Ok();
}
}
the controller just handles the request and orchestrates, in this case we're just putting the business logic of how to sign up a user into the controller
ero
eroOP•3y ago
i don't exactly... get the point of telling me this? i know that
Patrick
Patrick•3y ago
because you don't know what a controller is
ero
eroOP•3y ago
giving me the code doesn't tell me what a controller is
Patrick
Patrick•3y ago
the controller just handles the request and orchestrates, in this case we're just putting the business logic of how to sign up a user into the controller
ero
eroOP•3y ago
the user controller controls things that have to do with the user cool
Patrick
Patrick•3y ago
ASP.NET Core is a phone book, a controller is just a name for a class that is a page in that phone book, every public method in that controller is a phone number someone can call.
ero
eroOP•3y ago
hm
Patrick
Patrick•3y ago
i can't do much with "hm", i suppose you're not interested in me helping which is all you need to say. my final advice is; since you haven't done this before you don't need to over engineer it. Scalability of users rarely depends on class structure or frameworks you use šŸ‘
ero
eroOP•3y ago
i don't want to publish an incomplete product i want it to be perfect the moment it goes online
Patrick
Patrick•3y ago
won't happen, that's not how software is built
ero
eroOP•3y ago
it's supposed to be a competitor to another website. what's the point in releasing something that isn't already better
Patrick
Patrick•3y ago
and your competitor is out there growing and improving, while you're here pondering class structure
ero
eroOP•3y ago
i want it to be easy to read, easy to understand, easy to maintain, and easy to contribute to
Patrick
Patrick•3y ago
competing in the software space is about MVPs
ero
eroOP•3y ago
while also being fast, and feature complete
Patrick
Patrick•3y ago
here's what's going to happen - you build your software today on the requirements/priorities you think are important. You release the software. You're going to run into problems. Your users are going to request features, your priorities change. Suddenly the beautiful architecture you have isn't proven, falls apart and you need to reprioritise. this is why software engineers exist; no one builds or ships perfect products
Timtier
Timtier•3y ago
I do understand the desire to get a "perfect" or "complete" solution. Though I often have had projects grind to a halt because of it so I would be hesitant to start small and improve iteratively instead I follow "Done is better than perfect" as I otherwise get stuck with an overengineered solution; nicely designed but potentially overkill for what the customer needs I'd recommend to first and foremost look into; how will what you create have the competitive edge over other leaderbord services/sites that you are competing against? And put your eggs in that basket. Just start simple to realize the basic functionality for that, gather feedback and iterate on that. Often, simplest code is best anyhow You can still go with the flow of your source generator and project layout, but will you need it for the key functionality your API needs? I still have to a bunch prev messages so apologies if I make any assumptions
ero
eroOP•3y ago
i mean again, i just don't know where to start like it's not that i don't know how to do it that's part of it but i also just don't know how to even begin there's so much i need to do, and so much i don't know, it's like impossible to do anything and i can't even lay it all out! because i don't know how to best put things together, or what's even possible
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
just any of the dozens which are .net 7 minimal api tutorial playlists right
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
Yeah very few changes in asp.net from 6 to 7
ero
eroOP•3y ago
i don't even really know like... what to do first? like do i create users first? series? leaderboards? categories? submissions? posts? or do i not create any models at all first and worry about the database instead setting up postgres and whatever
Pobiega
Pobiega•3y ago
Could start with sqlite or an in-memory EF database while you learn the ropes no reason to go ham with migrations and stuff before you know what you are doing tbh And doing github + google + manual users all at once will take some tinkering to get going, authentication can be a lot of work
ero
eroOP•3y ago
ugh i don't know what to do :/
Pobiega
Pobiega•3y ago
Start out small.
Timtier
Timtier•3y ago
And if you dont know how to start; go by your data structure and start with the objects that do not depend on the others
ero
eroOP•3y ago
i can't think of a single thing that doesn't depend on something else
Timtier
Timtier•3y ago
What is a serie and what does it do?
ero
eroOP•3y ago
a series is a collection of 1 or more leaderboards it has 1 or more moderating users with one of those users being the "owner" so to speak (the moderator with the most rights)
Timtier
Timtier•3y ago
I see
ero
eroOP•3y ago
a series also has a forum, a user which requested the series in the first place (this is the user who is made "owner" initially. the owner can change later), and a user which accepted the series (a site admin) or rejected it, as the case may be
Timtier
Timtier•3y ago
And a form haa said posts, a leaderboard has submissions?
ero
eroOP•3y ago
a forum is a collection of 0 or more posts, where a post is something like a linked list (child posts == comments) a post can be locked (or not) and stickied (or not) maybe i'll make a distinction between a ForumPost and a normal Post a leaderboard is a collection of 0 or more Categories where there'll have to be a distinction between level categories and game categories, maybe they'll certainly be displayed separately a category has a lot of settings, so does a leaderboard and a category is basically a collection of submissions also a category must be either go by time, or by score INumericalMetric, if you will
Timtier
Timtier•3y ago
So in this case, the different type of users might be the only entity that doesn't have any direct dependencies? So we could start with the different user types
ero
eroOP•3y ago
"different types"?
Timtier
Timtier•3y ago
Previously you described an owner and a site admin
ero
eroOP•3y ago
why should those be different? can't i just give them like a Role enum? or use claims or whatever they're called
Pobiega
Pobiega•3y ago
Yup, either claims or roles would work here. roles likely being the easiest.
ero
eroOP•3y ago
but also a User can be the Owner of a Series, a Leaderboard, or a (Forum)Post the "owner" wasn't about the website
Pobiega
Pobiega•3y ago
well, isnt that more that the Series etc has an Owner prop?
ero
eroOP•3y ago
i suppose
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
maybe it's pretentious, but i think this is far from simple
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Timtier
Timtier•3y ago
Even so, nothing wrong with trying
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Timtier
Timtier•3y ago
So going back to the previous models discussed Ero, let's say I'd want to create a Series on your frontend, what would I need to fill in?
ero
eroOP•3y ago
(singular of series is series) i mean come on i even put it in parens to show that i don't care about it that much
Timtier
Timtier•3y ago
I know I know šŸ˜‚
ero
eroOP•3y ago
you have to provide the name of the series, you have to provide which leaderboards go into it (a minimum of 2), and you have to be the Owner of at least one of those leaderboards such a series request opens a "post" where a site admin and the requesting user can discuss things if needed (kinda like a pull request)
Timtier
Timtier•3y ago
I see. I assume those leaderboards are pre-existing leaderboards or do you create them as part of the creation of a series?
ero
eroOP•3y ago
they have to be pre-existing
Timtier
Timtier•3y ago
Alright So based on this, if I were to mock what I would send to the backend, I would send something along the lines of (probably simplified):
{
"name" : "Name of series",
"leaderboards": [1, 2, 3]
}
{
"name" : "Name of series",
"leaderboards": [1, 2, 3]
}
The name being input that I fill in
Pobiega
Pobiega•3y ago
You'll likely want to model that as some kind of "SeriesRequest" entity, that can be created if conditions are fulfilled (owner of at least one included leaderboard etc), that then has its own lifecycle. Only once accepted does it actually "turn into" a new series.
ero
eroOP•3y ago
i mean you wouldn't send anything CreateSeries would not be AllowAnonymous
Timtier
Timtier•3y ago
Right, but assuming I am a logged in user, this is what I would send. I will get back to that part
Pobiega
Pobiega•3y ago
CreateSeriesRequest, would users not be able to do that?
ero
eroOP•3y ago
i mean there's another layer of abstraction by the frontend i'd guess
Pobiega
Pobiega•3y ago
AcceptSeriesRequest obviously requires some form of admin rights even so, your backend can never fully trust the frontend and should validate everything itself too
ero
eroOP•3y ago
of course
Timtier
Timtier•3y ago
Still, lets stay with the request for "creating" the series before the rest of the flow Start simple and all that
ero
eroOP•3y ago
but yeah, i guess the frontend would send the name of the series, the leaderboard IDs, and the ID of the user who created the request
Timtier
Timtier•3y ago
Yep, though the ID matters a bit on your implementation of authorization and how you'd pass that
Pobiega
Pobiega•3y ago
ID of the user is implied by being the user issuing the call to the backend, no?
ero
eroOP•3y ago
i don't know how frontend works
Timtier
Timtier•3y ago
But for the sake of simplicity, lets stay away from the user part
ero
eroOP•3y ago
it shouldn't be the user issuing the call, right? like that doesn't sound right
Pobiega
Pobiega•3y ago
via the frontend, it is
Timtier
Timtier•3y ago
It does, let me see if I can clarify it
ero
eroOP•3y ago
my frontend sends me data
Pobiega
Pobiega•3y ago
frontend is an application running on the users computer
ero
eroOP•3y ago
and the frontend should also check whether the request can be made at all
Pobiega
Pobiega•3y ago
thats fine but its possible to get around those šŸ™‚
ero
eroOP•3y ago
like a request with 1 leaderboard is invalid
Pobiega
Pobiega•3y ago
sure
ero
eroOP•3y ago
and a request where the user isn't an owner of any of the boards is also invalid
Timtier
Timtier•3y ago
Yep
ero
eroOP•3y ago
so the "make request" button should simply not be clickable
Pobiega
Pobiega•3y ago
this is stuff you'd slap in a FluentValidation validator or something, imho on the DTO
Timtier
Timtier•3y ago
Long story short, the backend will be the source of truth for that.
ero
eroOP•3y ago
i should not even be able to receive a request which is not valid
Timtier
Timtier•3y ago
Why not?
Pobiega
Pobiega•3y ago
Yeah. Your frontend wont allow users to click the button to issue the request if not, but even so, people can and will try and send invalid requests via devtools
ero
eroOP•3y ago
because i say so because if you cannot click the button, you cannot make a request
Pobiega
Pobiega•3y ago
wrong
Timtier
Timtier•3y ago
That's the fun part šŸ˜„
Pobiega
Pobiega•3y ago
devtools/insomnia/curl says you can šŸ˜›
Timtier
Timtier•3y ago
If I can use your API, I can, without using your front-end Which is spooky
Pobiega
Pobiega•3y ago
yeah. This is why we say "never trust the frontend"
Timtier
Timtier•3y ago
But which is why you at the minimum add your validation in the backend system Your frontend can also have validation, that's fine
Pobiega
Pobiega•3y ago
99.9% of all requests will be via your frontend and not manipulated, but there will be some that are entirely bogus or contain malicious input
Timtier
Timtier•3y ago
But to go back to the first part. So you send the name and leaderboard IDs that you wish to use to create a series
ero
eroOP•3y ago
if i don't send the user ID, how would i validate anything?
Timtier
Timtier•3y ago
Usually, you would not just send an ID, because that can be faked easily. You would send something within your request, like a token or something similar, that the backend can recognize and make use of.
ero
eroOP•3y ago
api tokens, right...
Timtier
Timtier•3y ago
Since you want to support Google users and such, this would be an OAuth token or something similar. Anyhow, don't worry about that part yet But once it reaches your backend, somewhere in your backend you will have to perform the checks you described, starting with; - Does this request have a token? - Do I recognize this token as a User of my system?
ero
eroOP•3y ago
right
Timtier
Timtier•3y ago
This might be some built-in functionality of ASP.NET, it might be some library or package, doesn't quite matter. Or well, not at this point of the discussion šŸ˜„ So if the request does not have a token or is not a recognized user, you would usually return a response along the lines of Unauthorized Status Code 401, which basically means; you tried to do something to my backend, but I do not allow you to
Timtier
Timtier•3y ago
Unrelated shoutout to https://http.cat/ šŸ˜„
HTTP Status Cats API
HTTP Cats
API for HTTP Cats
ero
eroOP•3y ago
no token would be bad request, no? wrong token would be unauthorized
Timtier
Timtier•3y ago
Hmmm. I would argue both are unauthorized The wrong auth is the same as using no auth You can use whatever you want but I generally keep BadRequest for; you're allowed to make a request, but you filled in something wrong. For example; a leader board you do not own
ero
eroOP•3y ago
that's unauthorized to me lol like "hey, you don't have rights to do anything here" as in, you're not authorized to unauthorized
Timtier
Timtier•3y ago
I get why you'd think that would be unauthorized, though usually after you pass the authorization as a user, people use BadRequest to say "Hi, I know who you are but you're not allowed to do this" Anyhow, you can use either, your implementation But lets say a user sends this request to create a series, goes past the check which verifies their token. They'll end up in the "actual" implementation of the endpoint
ero
eroOP•3y ago
but then why is passing the wrong token to a series creation request unauthorized?
Timtier
Timtier•3y ago
Because you're not authorized to perform the request. Performing meaning that the server will try to process it at all
ero
eroOP•3y ago
thoroughly confused
Timtier
Timtier•3y ago
At this point it doesn't care what the contents of the series creation request are or if they're correct The endpoint I chose as example is a bit annoying because of the ownership part of leaderboards. So to maybe simplify it a bit; what about if a user wants to make a post on a forum Completely different request of course The request towards the backend will probably contain something along the lines of; their post text and the ID of the forum they are posting on Similarly, it gets to the backend and checks (assuming part of your design here); hey, this endpoint requires a user, as I do not allow anonymous people to post. So it checks if you have a token and if it is a token of a known user. So in this example, lets assume yes for both; it will enter the endpoint logic. Endpoint logic being whatever you put in the Controller method or minimal API route.
Pobiega
Pobiega•3y ago
Regarding Authorization, you normally do that before entering the actual code that handles your request itself, ie the controller action or "endpoint" code. ASP.NET will look at the attributes you've assigned on the controller and action and on the users claims/roles etc and see if they are allowed to even access the route - only then does your code take over and can look at the specifics, such as "is the leaderboard specified one that you own?" business authorization takes place after API level authorization for example, at work our apis are quite large and in several places contain lines like SecurityAssert.StaffUserCanAccessPatient(dto.PatientId) just because you are a valid staff user with a care assignment doesnt mean you can access this patient.
ero
eroOP•3y ago
i mean, that seems obvious yeah
Pobiega
Pobiega•3y ago
yup. its just that ASP can block a lot of requests from even reaching that point, because their token is invalid or expired or doesnt contain the right roles meaning that for us to do the expensive stuff (database queries usually), we know you are quite likely to be a real user and not doing funky stuff
Timtier
Timtier•3y ago
So once a request enters your Create Series endpoints, the following would probably happen; First, if you add validation, the "stupid" validation comes first; is the name not empty, are all leaderboard ids above 0, etc. If all that's fine, you would get to the more "expensive" logic which would be the actual validation; do I own one of these leaderboards? Ofc this check entirely depends on your design but lets say you have some LeaderboardService which abstracts some logic away, it could be something like.
foreach(var leaderboardId in requests.leaderboard)
{
var leaderboard = await _leaderboardService.GetLeaderboardByIdAsync(leaderboardId);

if (leaderboard.Owner.Id == UserId)
{
// You own at least one, you set a bool here or something.
}
}
foreach(var leaderboardId in requests.leaderboard)
{
var leaderboard = await _leaderboardService.GetLeaderboardByIdAsync(leaderboardId);

if (leaderboard.Owner.Id == UserId)
{
// You own at least one, you set a bool here or something.
}
}
Pobiega
Pobiega•3y ago
and break the loop šŸ˜„
Timtier
Timtier•3y ago
Depends on the checks, its a dumb poc example Otherwise some sort of _leaderboardsService.GetUserLeaderBoards(UserId);, bla bla and then go through that collection to see if it contains at least one of the request leaderboard ids ^ but i digress If it matches your own rule of "I must own at least one" then you pass the request values to the logic that actually creates the series Which might at this point just be a dumb database insert, who knows How the rest of your application does its' logic depends entirely on how you design it You can take the approach of Pobiega and put it in the controller, like here: https://discord.com/channels/143867839282020352/1045068329502646372/1046411827317321819
MODiX
MODiX•3y ago
Pobiega#2671
[HttpGet("{id}")]
public ActionResult<User> GetUser(long id)
{
var user = _database.Users.FirstOrDefault(x => x.Id == id);

if (user is null)
{
return NotFound();
}

return user;
}
[HttpGet("{id}")]
public ActionResult<User> GetUser(long id)
{
var user = _database.Users.FirstOrDefault(x => x.Id == id);

if (user is null)
{
return NotFound();
}

return user;
}
Quoted by
<@!205052896599867392> from #Creating a backend in .NET (click here)
React with āŒ to remove this embed.
Timtier
Timtier•3y ago
Or a different way entirely I have my own gripes with that structure, though it is very quick to get something up and running with it
ero
eroOP•3y ago
see and that's why i want source gen right? having to make the services and validation all separately is like
Pobiega
Pobiega•3y ago
Oh yeah I don't do app logic in controllers. ĀÆ\_(惄)_/ĀÆ
Timtier
Timtier•3y ago
Services sounds a bit scary but eh
Pobiega
Pobiega•3y ago
I don't think you can generate all those parts, since they are not just copypaste
Timtier
Timtier•3y ago
That's just your logic, moved to a different class your controllers are generally stupid They verify requests, pass it along, possibly catch unexpected results, and give a response
Pobiega
Pobiega•3y ago
command+handler/service + fluentvalidation + functional result (Result<T>) is a very good start.
Timtier
Timtier•3y ago
Even command/handler/CQRS might be a bit overkill here But if you want reusability, source generator is not really the solution here It's an overly complex solution for the what, 10 models you are making at the most?
ero
eroOP•3y ago
hard to believe it'll only be 10
Timtier
Timtier•3y ago
Right, but even if you double it, still managable šŸ˜›
Patrick
Patrick•3y ago
Respectfully you need to actually start building something. All the architecture and frameworks here are over engineered when you can’t build a simple API There’s a reason why MS documentation for ASP.NET doesn’t include services, CQRS or other convoluted measures for architecture.
Timtier
Timtier•3y ago
I'd also say indeed; just start implementing one endpoint in one controller; as simple as possible Once that works, you can refactor it to be more "flexible". But at that point you have something working you can fall back to
ero
eroOP•3y ago
so uhh. how do i start?
Patrick
Patrick•3y ago
Open up your IDE of choice and start a new ASP.NET Core API project Then make your first controller and work on one action Use the swagger UI to interact with your API
ero
eroOP•3y ago
Alright yeah pobiega and i already did that earlier
Pobiega
Pobiega•3y ago
You could go on with the usercontroller we made maybe start experimenting with authentication too, as it seems to be very important to your application but start with just doing it locally, dont involve github/google auth for now
ero
eroOP•3y ago
i also need like 2FA, and 3FA for site staff from a project architecture point of view, where would you put the database? like in what namespace?
Pobiega
Pobiega•3y ago
add 2fa/3fa later database stuff usually goes in its own project, often called Infrastructure or Data, but you can just make that a namespace if you want ofc
ero
eroOP•3y ago
hm, i see
Pobiega
Pobiega•3y ago
the core class there is your DbContext child
ero
eroOP•3y ago
that much i've gathered what's that? classlib?
Pobiega
Pobiega•3y ago
yap everything is a lib, except the web api itself
ero
eroOP•3y ago
i mean????
Pobiega
Pobiega•3y ago
and the whole splitting in multiple projects is not a strict requirement either, its just common practice
ero
eroOP•3y ago
its output type is also Library?
Pobiega
Pobiega•3y ago
yeah why not?
ero
eroOP•3y ago
so the api is also a lib
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
oh, not the api
ero
eroOP•3y ago
yeah that's a mega no for me
Pobiega
Pobiega•3y ago
the api is a console app or whatever
ero
eroOP•3y ago
the 1 project thing is it?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
yup
ero
eroOP•3y ago
but wouldn't the csproj need to specify <OutputType>Exe</> for that? also, i'm torn on what to call the solutions
Pobiega
Pobiega•3y ago
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

</Project>
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

</Project>
this is what I have in the one I've been using as we "coded along" and it works fine šŸ˜›
ero
eroOP•3y ago
i guess it's the Sdk.Web part that changes it
Pobiega
Pobiega•3y ago
likely, yes
ero
eroOP•3y ago
if you do just dotnet new console, you have that tag so the site is leaderboards.gg, so i just went with LeaderboardsGG.Backend for the solution and project names
Pobiega
Pobiega•3y ago
thats fine.
ero
eroOP•3y ago
but i changed the namespace to Lbgg should that be LBGG, LbGg?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
does to me
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
iirc conventions would say Lbgg or LbGg two letter acronyms is the only thing that goes allcaps
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
even API gets turned into Api when you follow conventions
Timtier
Timtier•3y ago
LeaderboardsGG / Lbgg both are fine so ehhhh
Pobiega
Pobiega•3y ago
yup
Timtier
Timtier•3y ago
Id prefer the first one
ero
eroOP•3y ago
so should i change the project names too?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
I prefer to keep project name and namespace synced up
ero
eroOP•3y ago
does my dbcontext go into some namespace too?
Pobiega
Pobiega•3y ago
usually "top level" of your data project/namespace
Timtier
Timtier•3y ago
Basically every class you make will be in a namespace. Just further namespaces are applied depending on which folders you have
ero
eroOP•3y ago
oh lol no i know that
Timtier
Timtier•3y ago
So if you use those namespaces you're good šŸ˜„
ero
eroOP•3y ago
my infrastructure project needs to know about my domain project, right? so it knows about my entities?
Pobiega
Pobiega•3y ago
yup
ero
eroOP•3y ago
namespace Lbgg.Infrastructure;

public class LbggDbContext : DbContext
{
public LbggDbContext(DbContextOptions<LbggDbContext> options)
: base(options) { }

public required DbSet<User> Users { get; set; }
}
namespace Lbgg.Infrastructure;

public class LbggDbContext : DbContext
{
public LbggDbContext(DbContextOptions<LbggDbContext> options)
: base(options) { }

public required DbSet<User> Users { get; set; }
}
[HttpPost]
public async Task<IActionResult> Create(CreateUserDto dto)
{
if (await _db.Users.AnyAsync(u => u.Username == dto.Username))
{
return BadRequest();
}

await _db.Users.AddAsync(new()
{
Username = dto.Username,
Email = dto.Email,
Password = dto.Password
});

return Ok();
}
[HttpPost]
public async Task<IActionResult> Create(CreateUserDto dto)
{
if (await _db.Users.AnyAsync(u => u.Username == dto.Username))
{
return BadRequest();
}

await _db.Users.AddAsync(new()
{
Username = dto.Username,
Email = dto.Email,
Password = dto.Password
});

return Ok();
}
Patrick
Patrick•3y ago
You need savechanges but sure
ero
eroOP•3y ago
i've been told not to use AddAsync?
Patrick
Patrick•3y ago
Yes don’t use it avoid it if you can
ero
eroOP•3y ago
why is that?
Patrick
Patrick•3y ago
Because it’s a specialised method to allow for an operation to occur during the Adding process. In almost all cases all you’re doing is adding to a collection in memory which doesn’t need to be an async operation. Needless overhead and complication.
ero
eroOP•3y ago
so i know that sometimes you also return the created user entity should you really do that? it contains the password after all that sounds pretty bad to return
Patrick
Patrick•3y ago
You shouldn’t ever send any entity over the wire Dumping your entity model back to consumers of an API exposes primary and foreign keys, it shows your schema and tightly couples usage of your API with your database
ero
eroOP•3y ago
i meant like return Ok(createdUser);
Patrick
Patrick•3y ago
If createduser is an instance of your entity then my point stands
ero
eroOP•3y ago
mh
Patrick
Patrick•3y ago
If createduser is a DTO then fine
ero
eroOP•3y ago
i mean, what does "show my schema" mean? the entire backend is open source
Patrick
Patrick•3y ago
Exposes your database design, columns
ero
eroOP•3y ago
yeah it's. it's open source
Patrick
Patrick•3y ago
Sure that’s irrelevant
ero
eroOP•3y ago
i mean, doesn't that expose my database design more than anything lol
Patrick
Patrick•3y ago
Yes that’s by virtue of being open source… a couple hours ago you were concerned with building the perfect solution now you want to expose database entities over the wire and circumvent best practice Which is it?
ero
eroOP•3y ago
i don't want to do anything i asked if you should do that
Patrick
Patrick•3y ago
Right and I said no
ero
eroOP•3y ago
i was there
Patrick
Patrick•3y ago
Good luck.
ero
eroOP•3y ago
ok...?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
what's that got anything to do with what i said lol
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
i mean i already do...? i don't understand what's going on
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
because i've seen people do that
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
could i instead return something like a UserViewDto?
qqdev
qqdev•3y ago
Hi! Just joined What is your entity called?
ero
eroOP•3y ago
User!
qqdev
qqdev•3y ago
You usually just append Dto
ero
eroOP•3y ago
should i?
qqdev
qqdev•3y ago
Kinda depends. Some people always create dto classes I don't
ero
eroOP•3y ago
i don't know if that makes sense here
qqdev
qqdev•3y ago
I would do it if you are a beginner Don't think about it too much
ero
eroOP•3y ago
because i have a CreateUserDto. it's what's passed into the UsersController.CreateUser endpoint(?) then i would maybe have a GeneralUserViewDto which is perhaps passed when a user page is loaded
qqdev
qqdev•3y ago
I see! How would that differ from a UserDto?
ero
eroOP•3y ago
maybe a MinimalUserViewDto for clicking a user's profile picture from a forum post which perhaps yields a pop up like on discord
qqdev
qqdev•3y ago
Where is that View infix coming from?
Anton
Anton•3y ago
dto = view already AutoMapper is great btw, without it the dto mapping code will become unmaintainable, especially if you need to map nested things
qqdev
qqdev•3y ago
that's not true got big dtos at work and it is easy to maintain without AutoMapper Just implement ToDto() methods and you are good to go Or use AutoMapper if you don't want to deal with that and can bear another dependency
ero
eroOP•3y ago
i just don't see how i can use a singular dto for everything
qqdev
qqdev•3y ago
you don't have to do whatever makes sense
ero
eroOP•3y ago
especially because my CreateUserDto contains a Password property it just doesn't make sense to me
qqdev
qqdev•3y ago
it's fine to use multiple ones then just try to keep it slim as much as possible
Anton
Anton•3y ago
Not exactly. If you use EF Core and want to project the database entities, just ToDto is not enough, because EF Core needs expressions, so you'd have to repeat your mapping code for every request, or deal with lower lever expressions. This becomes bad if you have nested things, because then the amount of copypasta multiplies. Either copy pasta, or dealing with combining expression trees, which is no fun
qqdev
qqdev•3y ago
i don't use ef core so I can't tell what do dtos have to do with the database btw?
Anton
Anton•3y ago
It's even rougher for you then. If you want efficient queries, you'd have to select only the things you need in your sql queries
qqdev
qqdev•3y ago
it should be a dto <-> entity relationship you get the entity from the db and convert it to a dto and vice versa
Anton
Anton•3y ago
No, that's the incorrect way of doing it Doing that you fetch all data of that entity when you only need a subset The right way is to .Select into a dto, if you use efcore if you just write raw queries, that would mean select into dictionaries or anonymous objects or whatever, and then mapping into the target type
Timtier
Timtier•3y ago
If we're being a bit more strict, it should be Database Model <---> Domain Model <---> DTO Model
qqdev
qqdev•3y ago
Do you have an example? @AntonC To get familiar with the ef core approach
Saber
Saber•3y ago
either select out the properties like a normal select statement into the dto, or use an expression like
public static Expression<Func<Entity, Dto>> Projection => x => new Dto { ... };
public static Expression<Func<Entity, Dto>> Projection => x => new Dto { ... };
Anton
Anton•3y ago
With automapper, you do dbContext.Users.ProjectTo<MinimalUserDto>(), which would be equivalent to
dbContext.Users.Select(u => new MinimalUserDto
{
Name = u.Name,
Avatar = u.Avatar,
// no other properties, User has more props than MinimalUser
});
dbContext.Users.Select(u => new MinimalUserDto
{
Name = u.Name,
Avatar = u.Avatar,
// no other properties, User has more props than MinimalUser
});
qqdev
qqdev•3y ago
I see
Anton
Anton•3y ago
Which generates the following SQL
select Name, Avatar
from Users
select Name, Avatar
from Users
So it doesn't fetch the whole user
qqdev
qqdev•3y ago
So you could just implement ToDto() methods I guess? And use it on each property you actually need
Anton
Anton•3y ago
It just being a method is not enough, that thing you see in the argument of select is an Expression<Func<User, MinimalUserDto>>, it's not just a func It builds an actual expression tree
ero
eroOP•3y ago
my entities don't evne know about my dtos
Anton
Anton•3y ago
If you were to use a normal extension method instead, it would query the whole user and then do the mapping after the fact
ero
eroOP•3y ago
they're in different projects entirely
Anton
Anton•3y ago
or it would just be an error, I don't remember
qqdev
qqdev•3y ago
dbContext.Users.Select(u => new MinimalUserDto
{
Name = u.Name,
Avatar = u.Avatar,
Permissions = u.Permissions.ToDto(),
// no other properties, User has more props than MinimalUser
});
dbContext.Users.Select(u => new MinimalUserDto
{
Name = u.Name,
Avatar = u.Avatar,
Permissions = u.Permissions.ToDto(),
// no other properties, User has more props than MinimalUser
});
What about that?
Anton
Anton•3y ago
This would query all permissions, then map the permissions after the fact Or it's not even allowed possibly
Patrick
Patrick•3y ago
You can’t just run arbitrary C# in a database engine
Anton
Anton•3y ago
I don't remember
qqdev
qqdev•3y ago
I see, thanks
Patrick
Patrick•3y ago
Your projection has to be something that can be translated.
Anton
Anton•3y ago
So you need to either build the expression tree manually, or copy-paste
Patrick
Patrick•3y ago
Also stay away from automapper.
qqdev
qqdev•3y ago
ty, didn't touch ef core that much. always sticked to more bare-metal stuff which does not have that "quirks"
Anton
Anton•3y ago
AutoMapper does this for you, it builds the right expression tree It might be worse for perf, but it's way more maintainable like orders of magnitude
Timtier
Timtier•3y ago
Until you get into more complex mappings and then it becomes more of an annoyance šŸ˜›
Patrick
Patrick•3y ago
Less performant and less maintainable
qqdev
qqdev•3y ago
i am not a big fan of magic ✨ what happens if property names don't match?
Anton
Anton•3y ago
AutoMapper is plenty configurable
qqdev
qqdev•3y ago
how does it know that property names are supposed to match?
Anton
Anton•3y ago
It's convention-based. It has a bunch of predefined rules
Patrick
Patrick•3y ago
You end up writing the same code people try to avoid
Anton
Anton•3y ago
But you can add more fine grained things if you need to
Patrick
Patrick•3y ago
It’s quite funny to read configurations for automapper
Anton
Anton•3y ago
imo automapper is less about not writing that mapping code than not writing it more than once. without manually combining expression trees, you'd have to copy paste some code multiple times if you have many dtos, especially with shared bits
ero
eroOP•3y ago
actually, pobiega did a similar thing here, returning the entire database entity
qqdev
qqdev•3y ago
Stack Overflow
Can I reuse code for selecting a custom DTO object for a child prop...
When querying using Entity Framework Core, I am using expressions to convert to DTO objects, which works well for the object, and any child collections. A simplified example: Model: public class Mo...
qqdev
qqdev•3y ago
I just saw that you could turn that ToDto() methods into expressions which totally makes sense That's probably what I would do. But everyone can figure out what they like the most meowheart
ero
eroOP•3y ago
so after converting my database to an actual DbContext, i'm getting
System.AggregateException: 'Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Lbgg.Infrastructure.LbggDbContext Lifetime: Singleton ImplementationType: Lbgg.Infrastructure.LbggDbContext': Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContextOptions`1[Lbgg.Infrastructure.LbggDbContext]' while attempting to activate 'Lbgg.Infrastructure.LbggDbContext'.)'
System.AggregateException: 'Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Lbgg.Infrastructure.LbggDbContext Lifetime: Singleton ImplementationType: Lbgg.Infrastructure.LbggDbContext': Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContextOptions`1[Lbgg.Infrastructure.LbggDbContext]' while attempting to activate 'Lbgg.Infrastructure.LbggDbContext'.)'
qqdev
qqdev•3y ago
That's a dependency injection exception It is trying to create a LbggDbContext instance which requires DbContextOptions<T> which are not registered Did you add a AddDbContext(...) call?
ero
eroOP•3y ago
i haven't so i have an Id for my users which i'd like to be auto-incrementing first off, is that sensible, and if so, how do i do it?
Timtier
Timtier•3y ago
That is sensible and secondly, it depends on the database type you're using
Timtier
Timtier•3y ago
Timtier
Timtier•3y ago
So if you ensure you define your Key in your database model or context. You might also need to apply a migration (google the concepts as I can't explain them in a short time :x )
ero
eroOP•3y ago
System.InvalidOperationException: No database provider has been configured for this DbContext. A provider can be configured by overriding the 'DbContext.OnConfiguring' method or by using 'AddDbContext' on the application service provider. If 'AddDbContext' is used, then also ensure that your DbContext type accepts a DbContextOptions<TContext> object in its constructor and passes it to the base constructor for DbContext.
System.InvalidOperationException: No database provider has been configured for this DbContext. A provider can be configured by overriding the 'DbContext.OnConfiguring' method or by using 'AddDbContext' on the application service provider. If 'AddDbContext' is used, then also ensure that your DbContext type accepts a DbContextOptions<TContext> object in its constructor and passes it to the base constructor for DbContext.
this happens when i try to call my CreateUser endpoint
public class LbggDbContext : DbContext
{
public LbggDbContext(DbContextOptions<LbggDbContext> options)
: base(options) { }

public required DbSet<User> Users { get; set; }
}
public class LbggDbContext : DbContext
{
public LbggDbContext(DbContextOptions<LbggDbContext> options)
: base(options) { }

public required DbSet<User> Users { get; set; }
}
hm
qqdev
qqdev•3y ago
.
ero
eroOP•3y ago
yes it was the required keyword which makes total sense naw wait nevermind builder.Services.AddDbContext<LbggDbContext>(); is there anything else i need to do?
qqdev
qqdev•3y ago
I think you have to setup the options iirc Just look it up on the web. I gtg, see ya!
Patrick
Patrick•3y ago
builder.Services.AddDbContext<LbggDbContext>(d => d.UseSqlServer(...));
builder.Services.AddDbContext<LbggDbContext>(d => d.UseSqlServer(...));
or whatever provider you're using
Anton
Anton•3y ago
you'll need the connection string to the database, which usually comes from the configuration for development there's a cool extension method for app, EnsureDatabaseCreate<context> useful for setting up the db on new machines
ero
eroOP•3y ago
alright, getting back into it a little (at work, so can't do all that much) just using an in memory database for testing right now
builder.Services.AddDbContext<LbggDbContext>(opt => opt.UseInMemoryDatabase("Lbgg"));
builder.Services.AddDbContext<LbggDbContext>(opt => opt.UseInMemoryDatabase("Lbgg"));
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
i'll worry about it later, yeah what i did for the auto-incrementing ID for now is this in my user entity:
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public long Id { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public long Id { get; set; }
and this in my DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.Property(u => u.Id)
.ValueGeneratedOnAdd();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.Property(u => u.Id)
.ValueGeneratedOnAdd();
}
is that right?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
oh?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
huh other thing. is there any way to do enums properly in swagger?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
that's annoying
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
i mean it just shows the enum as an array of numbers
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
can't it's for pronouns
Esa
Esa•3y ago
Funky thing about enums is they are just spicy ints šŸ‘€
ero
eroOP•3y ago
which is a flag enum yeah i'm aware lol
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
yeah that doesn't exist in my version
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Esa
Esa•3y ago
Just a quick question if you don't mind me asking Ero, you seem knowledgeable about a lot of things that relates to memory, performance etc - but i'm a little surprised backend is unfamiliar to you. What do you usually work with since you know your way around C# but not specifically backend tasks?
ero
eroOP•3y ago
apparently it's obsolete and was removed
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
that's what i went with yeah
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
i mean i don't really see another way
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
i mean i'm not gonna use random ints that i forget what they mean... process memory, specifically reading and writing remote memory, injecting, the like so anyway. i've decided to make the namespaces Api.Users.DataTransfer.In and Api.Users.DataTransfer.Out. this obviously doesn't seem right, i've certainly never seen anyone else do it the In one contains for example the CreateUserDto which is the object passed into my controller by whoever is calling the endpoint the Out namespace for example contains GeneralUserDto, or MinimalUserDto since i don't super like this solution, are there any better suggestions?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Esa
Esa•3y ago
re. structure, I usually have this structure:
Controllers
Models
(all dtos in here etc - you could also place them in their respective folders, for example Controllers.Users.Models if you wanted to)
Users
(my UserController exists here along with its interface and any other files that relates to only that controller)
// any other controllers I offer will have their own folder similar to Users
Integration
Logging
Spotify
Domain
(all domain files for my spotify integration would be here)
Spotify.cs
SpotifyAuthorizationHandler.cs
// etc
Service
// and in here I'd put my own service logic
Controllers
Models
(all dtos in here etc - you could also place them in their respective folders, for example Controllers.Users.Models if you wanted to)
Users
(my UserController exists here along with its interface and any other files that relates to only that controller)
// any other controllers I offer will have their own folder similar to Users
Integration
Logging
Spotify
Domain
(all domain files for my spotify integration would be here)
Spotify.cs
SpotifyAuthorizationHandler.cs
// etc
Service
// and in here I'd put my own service logic
Just posting this as an alternative seeing as you asked for suggestions
Anton
Anton•3y ago
yes Ā AddEnumsWithValuesFixFilters in the package Ā Ā Ā Ā <PackageReferenceĀ Include="Unchase.Swashbuckle.AspNetCore.Extensions"Ā Version="2.6.12"Ā /> I think this effectively copies the enum declarations so both names and their int values
ero
eroOP•3y ago
i'm not really sure what to do next. i also have a big concern about how to store user data surely i can't just have a string Password property and what about Google or GitHub logins? don't those also need to be stored as users? or are those separate entities?
Anton
Anton•3y ago
auth is a pretty big topic, I'm figuring it out myself atm
Patrick
Patrick•3y ago
you would still have your own user saved, for your own data
Anton
Anton•3y ago
YouTube
ASP.NET Core Authentication and Authorization Tutorials
asp.net core authentication and authorization tutorials. Covering asp.net core cookie authentication and authorization, jwt token authentication and authoriz...
Anton
Anton•3y ago
check out these vids
Patrick
Patrick•3y ago
but they wouldn't have a password and instead have an identifier and way to store it in your database the passwords you store should never be plaintext and be hashed/salted BCrypt and several other popular libraries exist to make that painless.
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
does it make sense to have a sort of base entity? something like this maybe?
internal abstract class EntityBase
{
public Guid Id { get; set; }

public Instant CreatedAt { get; set; }
public Instant? RemovedAt { get; set; }
}
internal abstract class EntityBase
{
public Guid Id { get; set; }

public Instant CreatedAt { get; set; }
public Instant? RemovedAt { get; set; }
}
Instant is from NodaTime?
Pobiega
Pobiega•3y ago
Thats fairly normal, especially if your app has "all entities" fields except ID.
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
We have it at work, with very similar CreatedAt/UpdatedAt etc
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
i see... i mean i don't know, i feel conflicted about that interfaces to me most of the time are like "this is what i expose" and i don't really want to "expose" setting anything
Pobiega
Pobiega•3y ago
Depends on if you need composability in your entities I suppose.
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
If everything uses softdelete and CreatedAt/UpdatedAt timestamps, you dont need it.
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
I like the composable interfaces a lot.
ero
eroOP•3y ago
also is the bool IsDeleted really necessary if the DeletedAt instant is nullable? i don't really think it makes sense to make it non-nullable
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
Instant? UpdatedAt as well, imo
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
i guess that one you can set to the same time as CreatedAt
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
normally if you care about when something happened but its optional, letting the timestamp be nullable is a good option and if you check this a lot, you can just add a helper property for the bool value public bool IsDeleted => DeletedAt is not null;
ero
eroOP•3y ago
i don't expect to be checking it a lot not yet anyway like one use i can think of is checking whether a username is available after a user chose to delete their account like wait 30 days after or something
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Pobiega
Pobiega•3y ago
hm, not sure tbh. I would explicitly ignore it with the entity configuration for sure
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
whatever that means do i need to set CreatedAt and UpdatedAt and all that manually? does that go in some method in my dbcontext class?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
the reason i wanted to use a base class is so i don't have to implement the things like ID and the Instance on every single one of my entities every time cause i mean, that's just unnecessary code i feel like
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
i mean vs does too, it's just why explicitly have those properties in the entities
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
i can't think of a situation where i wouldn't want something to be soft deletable i think i would want to be able to track each and every single thing my site staff did
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
and be able to easily undo it
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Anton
Anton•3y ago
using base classes as mixins isn't flexible, but I guess it's better than boilerplate
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Accord
Accord•3y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
ero
eroOP•3y ago
i'm not super sure on what my next steps are. i have a "working" user controller (literally only posts and gets by id), but it's obviously far from done i was thinking... setting up a proper database? i was thinking postgres or, perhaps work on encrypting passwords?
Kouhai
Kouhai•3y ago
If you're using EF, swapping your db drivers shouldn't require many modifications
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
people have recommended BCrypt?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
i don't really understand why you would hash a password?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
don't you need to decrypt it to check for a valid login?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Kouhai
Kouhai•3y ago
Encrypting passwords would mean the person with the access to the key can decrypt all passwords
ero
eroOP•3y ago
hm so, what about bcrypt? is it like, the "industry standard"?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
i see
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Anton
Anton•3y ago
you can use microsoft identity since you're already using ef core
ero
eroOP•3y ago
what's that?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Anton
Anton•3y ago
it dsows everything for you basically
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
no i mean ef core, what's that sorry, trolling
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
thought it was funny
Accord
Accord•3y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
ero
eroOP•3y ago
How would one implement a "edit history" feature, say for posts? I'll be honest, i haven't worked on anything since i last posted here, just a bit busy and also not feeling like working on something i don't understand lol
Patrick
Patrick•3y ago
another table that retains versions of posts
class Post
{
int Id
string Content
ICollection<PostVersion> Versions
}

class PostVersion
{
int Id
int PostId
Post Post
DateTimeOffset CreatedDateTime
string Content
}
class Post
{
int Id
string Content
ICollection<PostVersion> Versions
}

class PostVersion
{
int Id
int PostId
Post Post
DateTimeOffset CreatedDateTime
string Content
}
ero
eroOP•3y ago
Hm, so I'd add the post's current information into a new instance of a PostVersion, add that to that post's Versions, and then overwrite it with the new information?
Patrick
Patrick•3y ago
yes
ero
eroOP•3y ago
And i guess i could do that in overwrite SaveChangesAsync?
Patrick
Patrick•3y ago
no, that would just be part of your service
ero
eroOP•3y ago
Mh
Patrick
Patrick•3y ago
there's no overriding of ef behaviour
ero
eroOP•3y ago
What? Sure there is In my DbContext?
Patrick
Patrick•3y ago
no, if you're overriding save changes for some business logic it's generally incorrect
ero
eroOP•3y ago
What if i want to change the entity's UpdatedAt time?
Patrick
Patrick•3y ago
i do that in my services ĀÆ\_(惄)_/ĀÆ
ero
eroOP•3y ago
You guys fight it out i guess lol
Patrick
Patrick•3y ago
yeah i dont like that - every entity is beholden to that logic what if i change a property on an entity via the system and it shouldn't update the last updated column
ero
eroOP•3y ago
It should, at least in my opinion
Patrick
Patrick•3y ago
code like this is hidden from developers and is why EF gets a bad wrap obscuring what happens when i call SaveChanges by injecting business logic, without a developer knowing, is less maintainable, readable and leads to debugging sessions asking wtf is going on
ero
eroOP•3y ago
Lol i had to Google that.
Bad wrap is considered wrong and is best saved for referring to wraps and tortillas.
– Merriam-Webster Found that funny
Patrick
Patrick•3y ago
it's autocorrect it's bad rap, not wrap
ero
eroOP•3y ago
I believe it, don't worry It's rep From reputation
Patrick
Patrick•3y ago
no bad rap is an idiom i mean bad rap
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Patrick
Patrick•3y ago
i dont know what that interface does
ero
eroOP•3y ago
Ah, you're right
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Patrick
Patrick•3y ago
think about the semantics of it, "savechanges" but it actually sets some properties code like this is only made possible because EF allows you to override save change behaviour, if you didn't have EF how would you implement it with Dapper?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Patrick
Patrick•3y ago
yes
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Patrick
Patrick•3y ago
that's an entirely different concept multi tenanted apps are not on the same level as "im calling savechanges" initialising a db connection and having that go to the right tenant in a multi tenanted application is pretty standard nor is it hidden code, because the initialisation is expected to... well... initialise global query filters are as evil as this - let me just add a global filter real quick and have every query in my system affected by this, unless i opt out you don't get global query filters in sql, so why should i let some magic dictate that for me again - this is EF's doing and habits like that kill maintainability
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Patrick
Patrick•3y ago
sure, it's just a code smell
ero
eroOP•3y ago
I'm gonna agree with Duke i think, i prefer not forgetting to update all that stuff, especially since all my entities will be softdeletable and lots will be updateable and versionable
Patrick
Patrick•3y ago
easy to fall into the trap when you're new to this šŸ™‚
ero
eroOP•3y ago
I just don't see the issue really
Patrick
Patrick•3y ago
obscuring what happens when i call SaveChanges by injecting business logic, without a developer knowing, is less maintainable, readable and leads to debugging sessions asking wtf is going on
i won't bother to mention the perf hit
ero
eroOP•3y ago
I don't know, i just disagree? Save changes means applying all changes to the db. That implicitly means updating the UpdatedAt time to me
Patrick
Patrick•3y ago
to you being the key the argument against this is you forget you can also forget to add the interface then what?
ero
eroOP•3y ago
I'm not sure how you mean Forget it where?
Patrick
Patrick•3y ago
forget the interface
ero
eroOP•3y ago
Yes, where?
Patrick
Patrick•3y ago
your class
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
How would I forget it? It's the very thing that makes something versionable Exactly
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
Patrick
Patrick•3y ago
so an interface dictates business behaviour? the mere presence injects behaviour that you can't see anywhere in the entity or service or anywhere discernible other than your database layer because it's the argument against this you're human and forget things everyone does
ero
eroOP•3y ago
This is incredibly doubtful, I'm gonna be real If this is the only way I've ever worked like in my project, it's unlikely I'll change how i work on a whim
Patrick
Patrick•3y ago
not impossible, then
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
FusedQyou
FusedQyou•3y ago
Damn, my man is creating a whole project in this thread
Accord
Accord•3y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
ero
eroOP•3y ago
@Pobiega could you look over this and offer some advice?
[AllowAnonymous]
[HttpPost("register")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> RegisterUser(RegisterUserRequest request)
{
if (await _db.Users.AnyAsync(u => u.Email == request.Email))
{
return Conflict("A user with this email address already exists.");
}

if (await _db.Users.AnyAsync(u => u.Username == request.Username))
{
return Conflict("A user with this username already exists.");
}

User registeredUser = new()
{
Username = request.Username,
Email = request.Email,
Password = BCryptNet.EnhancedHashPassword(request.Password)
};

_db.Users.Add(registeredUser);
await _db.SaveChangesAsync();

// TODO: Create response type.
RegisterUserResponse response = new(
);

return CreatedAtRoute($"{registeredUser.Id}", response);
}
[AllowAnonymous]
[HttpPost("register")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> RegisterUser(RegisterUserRequest request)
{
if (await _db.Users.AnyAsync(u => u.Email == request.Email))
{
return Conflict("A user with this email address already exists.");
}

if (await _db.Users.AnyAsync(u => u.Username == request.Username))
{
return Conflict("A user with this username already exists.");
}

User registeredUser = new()
{
Username = request.Username,
Email = request.Email,
Password = BCryptNet.EnhancedHashPassword(request.Password)
};

_db.Users.Add(registeredUser);
await _db.SaveChangesAsync();

// TODO: Create response type.
RegisterUserResponse response = new(
);

return CreatedAtRoute($"{registeredUser.Id}", response);
}
the thing here is that see people use services for things like those checks (whether a user with the given email or username already exists) so for example they have _userService.GetByUsername() and _userService.GetByEmail() and i'm not sure whether that's really required, if there's a better way i feel like doing 2 queries, once for email and once for username, is just unnecessary also is CreatedAtRoute correct here? should i return something else? i see CreatedAtAction being used, with a GetUserById endpoint in the controller
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
Necessary comment
Pobiega
Pobiega•3y ago
So, this is fine, but having application logic in the controller directly is controversial, but it's a valid choice Doing one query could work, but might give a worse error message back, which may or may not be desired
ero
eroOP•3y ago
What's your suggestion?
Pobiega
Pobiega•3y ago
Probably passing the entire request object into either a command handler or a service class method, and copy-pasting the entire method minus the HTTP specific bits (conflict, createdatroute) into there that would be the "other way" to do things. If this project will only ever be a web api, that isnt strictly needed and for shorter methods like this, this is tbh fine
ero
eroOP•3y ago
which one do you think is more, i guess modern? more maintainable, more easily expandible?
Pobiega
Pobiega•3y ago
Imho command handlers, but as said, this is controversial and everyone has their own opinions you'll probably need something like a Result<T> type if you go that way, so that the handler can indicate what went wrong (was the email/username taken, was your password not long enough, etc) and let hte controller return the correct http response as long as you keep your controllers focused and specific, its fine to inline code in them. It becomes a problem when you have 500+ lines of code controllers that know everything about your app
ero
eroOP•3y ago
where in your solution would you say you'd put those?
Pobiega
Pobiega•3y ago
the handlers? in your application logic/business logic project, usually grouped by some kind of feature. Like, a UserRegistrationHandler would live in the App.Business.Users namespace and if you are fine with having multiple types in a single file, I'd probably put my command in the same file as the handler as they are extremely closely related
ero
eroOP•3y ago
so i have an App.Api.Users namespace, which has my UsersController. would you still say the handler goes into a App.Business.Users namespace?
Pobiega
Pobiega•3y ago
If you have an App.Business project, yes if you dont, then its fine to have it in your App.Api
ero
eroOP•3y ago
mh, i have an App.Shared project?
Pobiega
Pobiega•3y ago
isnt that more for DTOs and contract classes?
ero
eroOP•3y ago
which has my request and response records
Pobiega
Pobiega•3y ago
right, so thats not the place for a handler
ero
eroOP•3y ago
oh so the "command" isn't the request
Pobiega
Pobiega•3y ago
it should be where your core logic is, wherever that might be well, it can be there are certainly cases where you can literally use a request as your command object
ero
eroOP•3y ago
hm
Pobiega
Pobiega•3y ago
and just have a handler use that. sorta depends on what level of validation you do and where
ero
eroOP•3y ago
so considering this
Pobiega
Pobiega•3y ago
then probably in your API
ero
eroOP•3y ago
i feel like i need to change this a bit i'm find with adding an App.Business project, no harm there i'm just still a bit confused on how things are split up mainly Infrastructure and Domain
Pobiega
Pobiega•3y ago
thats understandable, there are thousands of ways to do this šŸ˜›
ero
eroOP•3y ago
the main concerns apply make it easy for contributors to find their way around and make contributions easily
Pobiega
Pobiega•3y ago
well, domain is the core things that your program revolves around - users, categories, time series, individual submissions etc
ero
eroOP•3y ago
make the code concise, maintainable, and performant "time series"?
Pobiega
Pobiega•3y ago
infrastructure will contain your EF specific stuff related to those core things, like your configuration classes, your dbcontext, etc
ero
eroOP•3y ago
just an example of an entity?
Pobiega
Pobiega•3y ago
well you were making a speedrun website? yeah
ero
eroOP•3y ago
right yeah not just speedrunning, score "runs" too but yeah
Pobiega
Pobiega•3y ago
yeah it was just an example of an entity
ero
eroOP•3y ago
alright good so the command handlers just genuinely don't go in either domain or infra this is a lot
Pobiega
Pobiega•3y ago
not "traditionally" no, they are what contains most of your application logic some people have "had it" with this entireprise splitting of everything, and there are merits to that approach too
ero
eroOP•3y ago
that's what the project is doing currently, and i'm not a huge fan of it... it's just very cluttered would you say validation goes into App.Business then too? https://github.com/leaderboardsgg/leaderboard-backend here's the current setup, by the way. feel free to absolutely criticize every little thing some of us know absolutely no db stuff, others are new to c#. so it's a bit of a mess which is why i'm planning this refactor get myself familiar with db things, the c# part is easy for me
Pobiega
Pobiega•3y ago
Sure, any explicit validation. You get some for free by ASP.NET, like respecting required and stuff
ero
eroOP•3y ago
would you say making up a custom identification type is worth it? the site needs an id for each individual submission, which can become a lot. we're thinking of using something like discord's snowflakes; a 64 bit number where part of it consists of a timestamp and another of an incrementing number
Pobiega
Pobiega•3y ago
ĀÆ\_(惄)_/ĀÆ if you use long for IDs, they grow very large without needing snowflake style IDs but if you dont want enumerability, then go for a guid or a snowflake
ero
eroOP•3y ago
imo the issue with guid is accessing a page via it? like !e $"https://lb.gg/user/{Guid.NewGuid()}"
MODiX
MODiX•3y ago
Ero#1111
REPL Error
An error occurred while sending a request to the REPL service. This may be due to a StackOverflowException or exceeding the 30 second timeout. Details: Connection refused (repl:31337)
Tried to execute
ero
eroOP•3y ago
aw really i mean you get the idea
Pobiega
Pobiega•3y ago
yeah urls get nasty with guids in them
ero
eroOP•3y ago
and auto-incrementing sort of enables this "race" to get a low ID somewhere
Pobiega
Pobiega•3y ago
and enumerability so yeah, if you dont like that, go for a snowflake
ero
eroOP•3y ago
is enumerability a bad thing?
Pobiega
Pobiega•3y ago
can be like, if my user profile page is lb.gg/user/6123, what stops me from scraping all the users?
ero
eroOP•3y ago
the rate limit of the website! or something
Pobiega
Pobiega•3y ago
true obviously more of a problem if there is some valuable information on that page, like linkedin etc
ero
eroOP•3y ago
i guess that's a valid concern though
Pobiega
Pobiega•3y ago
only "downside" will be that you wont be able to have your database generate IDs, but thats easy enough to work around
ero
eroOP•3y ago
could i have guids as the internal storage, but get them via some other converted value? guids do store their bytes after all
Pobiega
Pobiega•3y ago
ĀÆ\_(惄)_/ĀÆ never tried easily solved by having the public entity ctor assign a snowflake, and have a protected one for EF to use
ero
eroOP•3y ago
i don't even know how to begin doing that lmao
Pobiega
Pobiega•3y ago
just have your "first" constructor be a protected one and have an argument for every property EF will use that to create its entities and when you make new users or whatever with new User() you use the public ctor, that creates a snowflake as part of object initialization as for generating the snowflake, thats on you, i have no idea how they work šŸ™‚
ero
eroOP•3y ago
how would i tell EF that my custom snowflake type is the primary key? just slap that attribute on it?
Pobiega
Pobiega•3y ago
call the property Id šŸ™‚
ero
eroOP•3y ago
ah, right it does do that
Pobiega
Pobiega•3y ago
or attribute, or set it with your EntityConfiguration<T> class, or in the OnModelCreate (dont do this)
ero
eroOP•3y ago
should things like migrations be pushed to the source repo?
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
they don't
Unknown User
Unknown User•3y ago
Message Not Public
Sign In & Join Server To View
ero
eroOP•3y ago
why should other people besides us get a working copy of the db lol
Pobiega
Pobiega•3y ago
Migrations are... a tradeoff. They generate a database snapshot for each migration, so in a long-living application with hundreds of migration, a very significant part of your compile time will be those snapshots however, once this thing goes live you dont want to have to restart the DB from scratch every time there is an update
Accord
Accord•3y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
ero
eroOP•3y ago
do you mind It does this way too quickly man
Patrick
Patrick•3y ago
24 hours isn’t way too quickly
ero
eroOP•3y ago
disagree i guess? what an unnecessary comment
Patrick
Patrick•3y ago
I’m glad you consider a comment telling you 24 hours isn’t too quickly as unnecessary in the face of a post that has well exceeded the general use case of the help forum we have on this channel as unnecessary. Consider moving this thread elsewhere if you believe the tools that we have in place that fit the general use of the forum as invasive.
ero
eroOP•3y ago
suggest a place to move the thread to
Patrick
Patrick•3y ago
Looks like code review to me, now.
Gooster
Gooster•3y ago
nearly 1500 messages <:PES_OMGNO:681925594312736811>
Accord
Accord•3y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.

Did you find this page helpful?