In this challenge you will build an API, write custom middleware and filters that satisfies the requirements
listed under the Middleware and Filter Requirements
section.
Completion of this project demonstrates working understanding of
- .NET Pipeline
- Middleware
- Action Filters
- Http Context
- Request Validation
.NET 7 SDK
- Create a new empty
solution
and call it whatever you want egMiddlewareAndValidation
. If using .NET CLI Tool usedotnet new sln -n <project_name>
. - Create two new Projects
Api
: Web API Project, this is where Controllers, Mapping Extension methods and API Contracts will live.Application
: Class Library, this is where our Models and Repositories will live along with Extension Methods for project registration.
Create a reference where Api
project references the Application
project.
If you need help with project creation and referencing using the dotnet
cli tool reach out for help or you can also use
dotnet --help
and dotnet new --help
to help figure out the needed commands.
Application
- Microsoft.Extensions.DependencyInjection.Abstractions: required for being able to register the
Application
project properly
In the Api
project create a folder called Contracts
and inside of Contracts create two classes
CreatePostRequest
: Represents a incoming request to make a newPost
UserUpsertRequest
: upsert means Update / Insert, this class is used for incoming requests that update or create a user.
These two classes will be used to represent the request body
for incoming POST / PUT requests to the API for creating or updating
Posts or Users respectively.
In order to get the Application
and In-Memory
database setup correctly we will need to do the following...
- Setup repositories
// File: Application/Respositories/UserRepository
public class UserRepository : IUserRepository
{
// in-memory DB
private readonly List<User> _users = new();
// your repository methods here...
}
// File: Application/Repositories/PostRepository
public class PostRepository : IPostRepository
{
// in-memory DB
private readonly List<Post> _posts = new();
// your repository methods here...
}
- Setup Registration File
// File: Application/ApplicationServiceCollectionExtensions
public static class ApplicationServiceCollectionExtensions
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddSingleton<IUserRepository, UserRepository>();
services.AddSingleton<IPostRepository, PostRepository>();
return services;
}
}
- Register the Application project
// File: Api/Program.cs
var builder = WebApplication.CreateBuilder(args);
// Other code...
builder.Services.AddApplication();
var app = builder.Build();
Important Note If your application is forcing HTTP requests and giving you odd behavior related to HTTPS you can delete this line here...
var app = builder.Build();
// Other code...
app.UseHttpsRedirection(); // <- remove this line and restart server
-
Logger
- Create a
logger middleware
that logs to the console the following information about each request in this format:[MiddlewareAndValidationLogger] {request_method} | {request_url} | {timestamp}
- this middleware runs on every request made to the API
- Create a
NOTE: To send a response from a filter use the
Result
property on theActionExecutingContext
object Red on the ActionExecutingContext here for more
-
Validate UserId Filter
- this filter will be used for all user endpoints that include an
id
parameter in the url (ex:/api/users/:id
) and it should check the database to make sure there is a user with that id. - if the
id
parameter is valid, store the user object onHttpContext
and allow the request to continue - if the
id
parameter does not match any user id in the database, respond with status404
and{ message: "user not found" }
- this filter should only be able to be used on
Controller Actions
. - Important Note have your filter inherit from the
Attribute Class
and implement eitherIActionFilter
orIAsyncActionFilter
- this filter will be used for all user endpoints that include an
-
ValidateUser
- Using Data Annotations validate the
body
on a request to create or update a user - if the request
body
lacks the requiredname
field, the API should respond with a Validation Error
- Using Data Annotations validate the
-
Validate Post
- Using Data Annotations validate the
body
on a request to create a new post - if the request
body
lacks the requiredtext
field, the API should respond with a Validation Error
- Using Data Annotations validate the
IUserRepository
CreateAsync
: calling CreateAsync passing it a User will add the user to the database and return true when successfully added.ExistsAsync
: calling ExistsAsync passing in auserID
will return a User if the user exists, else returns null.
IPostRepository
CreateAsync
: calling CreateAsync passing it aPost
will add the post to the database and return true when successfully added.
All methods should return a Task, when returning use Task.FromResult
The Database Schemas for the users
and posts
resources are:
field | data type | metadata |
---|---|---|
id | Guid | primary key |
name | string | required, unique |
field | data type | metadata |
---|---|---|
id | Guid | primary key |
text | text | required |
user_id | Guid | required, must be the id of an existing user |
These tasks are less hand held than up above. Tasks vary in level of difficulty.
If you would like to implement these requirements, you are going to have
to bridge the gap by researching about .NET topics such as Model Binding
, Model Validation
, ProblemDetailsObject
out parameters
and other items.
[Routes and Data Access]
You may have noticed this API is not feature complete. Implement missing routes so that full CRUD can be performed on both User and Post
resources including getting ALL Posts / Users or finding a User / Post by id.
Implement any other functionality needed in the repositories
to help support the newly created routes and their functionality.
[Filter] Validate UserId Filter
instead of the filter responding with { message: "user not found }
construct a ProblemDetails
object
Setting the Title
to Validation Failed
, Status
to 400
, Instance
to the requested URL (this should come from the HTTP Context)
Add the key userId
and error of user with the provided id does not exist
[IUserRepository Method] ExistsAsync
Create a overload
of ExistsAsync, that accepts two values a userId
and a out parameter
for the user
your out parameter should be of type User?
(nullable user). If a user exists return true
and assign the
out paramter
to the found user. Else return false, and assign the user out parameter
to be null
[Mapping Layer]
In the Api
project create a folder called Mapping
with static
classes of UserMapping
and PostMapping
in these classes you will create extension methods
that converts types declared in Contracts
(created in step 1.b) to
types that are defined in Application.Models
respectively. You will want a way to map from Contracts
-> Models
and back
from Models
-> Contracts
. Your controllers are responsible for taking in a contract
and converting it to a model
before
calling on database actions and converting models back into contracts when sending out a response.
[Validation using Fluent Validation]
In the Api
project, we have our Contracts
. Our contracts were to be validated using Data Annotations. Pick one of the
contracts either UpsertUserRequest
or CreatePostRequest
and remove the data annotation(s).
Install the NuGet Package FluentValidation.DependencyInjectionExtensions
to the Api
project
In Api
project create a new folder called Validators
and create a class called UpsertUserValidator
and write the needed
logic for validating the required field on the incoming request.
Create a new interface under Api
project called IApiAssemblyMarker
and use it to register all validators in the Api
assembly.
From there, create a new filter called UpsertUserActionFilter
and preform the needed validation on the incoming request.
-
If validation is in a failed state respond with a
ProblemDetailsObject
. Setting theTitle
toValidation Failed
,Status
to400
,Instance
to the requested URL (this should come from the HTTP Context). The key and value used here is going to be dynamic and come from thevalidation result
generated using yourUpsertUserValidator
. -
If validation is in a success state, then call on the next method in the pipeline.