ASP.NET .NET6 System.ArgumentNullException: "Value cannot be null. Arg_ParamName_Name" - c#

I have a TryGetByStatementId method, which, using the FirstOrDefault method, returns me the id from the database.
I also have a database that stores information about the uploaded file.
I upload the file through the controller, the file is saved in the file system in wwwroot, and information about it such as id, Name, CreateDateTime are saved in the database.
When I try to delete information using the TryGetByStatementId method, I get an error -> System.ArgumentNullException: "Value cannot be null. Arg_ParamName_Name"
The controller with which I download the file and save it to the file system
public class ManagementController : Controller
{
private readonly ApplicationDbContext applicationDb;
private readonly IWebHostEnvironment _webHostEnvironment;
public ManagementController(ApplicationDbContext applicationDb, IWebHostEnvironment webHostEnvironment)
{
this.applicationDb = applicationDb;
_webHostEnvironment = webHostEnvironment;
}
public IActionResult Index()
{
return View();
}
[HttpPost]
public async Task<IActionResult> AddFile(IFormFile uploadFile)
{
if (uploadFile != null)
{
string path = "/Files/" + uploadFile.FileName;
using (var fileStream = new FileStream(_webHostEnvironment.WebRootPath + path, FileMode.Create))
{
await uploadFile.CopyToAsync(fileStream);
}
StatementsDb file = new StatementsDb { StatementsName = uploadFile.FileName, CreateDateTime = DateTime.Now, Path = path};
applicationDb.statementsDbs.Add(file);
applicationDb.SaveChanges();
}
return RedirectToAction("Index");
}
}
Code snippet from the controller with the Remove method
public IActionResult RemoveFile(int statementId)
{
statementsDb.Remove(statementId);
return RedirectToAction("Index");
}
Code snippets from the entity in which I'm trying to get the ID
public StatementsDb TryGetByStatementId(int id)
{
return applicationDb.statementsDbs.FirstOrDefault(statement => statement.StatementsId == id);
}
public void Remove(int statement)
{
var existingStatement = TryGetByStatementId(statement);
applicationDb.statementsDbs.Remove(existingStatement);
applicationDb.SaveChanges();
}
Only afterwards, I get the error System.ArgumentNullException: "Value cannot be null. Arg_ParamName_Name"
Where did I go wrong?
[UPDATE]
using archivingsystem.db;
using archivingsystem.Helpers.AutoMapping;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
namespace archivingsystem
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
IMapper mapper = MappingConfig.RegisterMaps().CreateMapper();
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// AutoMapper
builder.Services.AddSingleton(mapper);
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
// Services
builder.Services.AddTransient<IStatementsDbRepository, StatementsDbRepository>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
}
}
}

This error is occurring because the existingStatement variable is null. you can add a check for null before calling the Remove method
if (existingStatement != null)
{
applicationDb.statementsDbs.Remove(existingStatement);

I think it might be a problem with your url, you may not get the statement, that is to say, the statement is always 0, so you cannot get the data. What is your url for the Remove request?
From your current code, your url should look like this:
https://localhost:xxx/Management/Remove?statement=1
At this point you can see that the statement is 1, and the id of TryGetByStatementId is also 1:
If you want to use a url like this:
https://localhost:xxx/Management/Remove/2
Then you need to add attribute route for Remove:
[Route("[controller]/[action]/{statement:int}")]
At this point you can see that the statement is 2, and the id of TryGetByStatementId is also 2:
If you can get the id correctly, then you should be able to get the corresponding data from the database.

I solved the problem like this:
public IActionResult RemoveFile(Guid statementId)
{
var removeF = statementsDb.TryGetByStatementId(statementId);
statementsDb.Remove(removeF.StatementsId);
return RedirectToAction(nameof(Index));
}
and
public void Remove(Guid statement)
{
var fileId = applicationDb.statementsDbs.FirstOrDefault(x => x.StatementsId == statement);
applicationDb.statementsDbs.Remove(fileId);
applicationDb.SaveChanges();
}

Related

Why won't my API's updated DbContext fetch a newly added DB column?

I have an Azure SQL DB that initially had the following columns:
user name
password hash
password salt
This DB serves a .NET Core C# API that checks username and password to return a JWT token.
The API had a User object that comprised all three columns with the correct types, a DbContext with a DbSet<User>, and an IServiceCollection that used said DbContext.
The API worked fine, returning a JWT token as needed.
I have since needed to add an extra parameter to check and pass to the JWT creation - the relevant column has been created in the DB, the User object in the API has been updated to include the extra parameter and that extra parameter is observed in the Intellisense throughout the API code.
The issue is that when the API is deployed to Azure, the extra parameter isn't being recognised and populated; how do I make the API correctly update to use the new DbContext and retrieve the User with the extra parameter?
(I've omitted the interfaces for brevity, as they're essentially the corresponding classes)
User, UserRequest and MyApiDbContext Classes:
using Microsoft.EntityFrameworkCore;
namespace MyApi.Models
{
// Basic user model used for authentication
public class User
{
public string UserId { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
public string ExtraParam { get; set; } // newly added parameter
}
public class UserRequest
{
public string UserId { get; set; }
public string password { get; set; }
}
public class MyApiDbContext : DbContext
{
public MyApiDbContext(DbContextOptions<MyApiDbContext> options)
: base(options)
{
}
public DbSet<User> Users { get; set; }
}
}
The AuthRepository that retrieves the user:
using Microsoft.EntityFrameworkCore;
using MyApi.Interfaces;
using MyApi.Models;
using System.Threading.Tasks;
namespace MyApi.Services
{
public class AuthRepository : IAuthRepository
{
private readonly MyApiDbContext _context;
public AuthRepository(MyApiDbContext context)
{
_context = context;
}
public async Task<User> Login(string username, string password)
{
// my test user gets returned
User returnedUser = await _context.Users.FirstOrDefaultAsync(x => x.UserId == username);
if (returnedUser == null)
{
return null;
}
// the password get verified
if (!VerifyPasswordHash(password, returnedUser.PasswordHash, returnedUser.PasswordSalt))
{
return null;
}
// this does not get changed, but the value set in the DB is definitely a string
if (returnedUser.ExtraParam == null || returnedUser.ExtraParam == "")
{
returnedUser.ExtraParam = "placeholder"
}
return returnedUser;
}
}
}
The AuthService that calls the AuthRepository for the user then "creates the JWT token" (just returning a string for this example), currently set up to return the user details:
using Microsoft.Extensions.Options;
using MyApi.Interfaces;
using MyApi.Models;
using System;
using System.Threading.Tasks;
namespace MyApi.Services
{
public class AuthService : IAuthService
{
private readonly IOptions<MyApiBlobStorageOptions> _settings;
private readonly IAuthRepository _repository;
public AuthService(IOptions<MyApiBlobStorageOptions> settings, IAuthRepository repository)
{
_repository = repository;
_settings = settings;
}
public async Task<string> Login(string username, string password)
{
User returnedUser = await _repository.Login(username, password);
if (returnedUser != null)
{
// currently returns "UserIdInDB,ProvidedPasswordFromLogin,"
return $"{returnedUser.UserId},{password},{returnedUser.ExtraParam}";
}
return null;
}
}
}
The controller that calls the AuthService:
using Microsoft.AspNetCore.Mvc;
using MyApi.Interfaces;
using MyApi.Models;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace MyApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly MyApiDbContext _context;
private readonly IAuthService _authService;
public AuthController(MyApiDbContext context, IAuthService authService)
{
_context = context;
_authService = authService;
}
[HttpPost("login")]
public async Task<IActionResult> Login(UserRequest loginUser)
{
string token = await _authService.Login(loginUser.UserId, loginUser.Password);
if (token != null)
{
return Ok(token);
}
return Unauthorized("Access Denied!!");
}
}
}
The startup class that registers everything:
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using MyApi.Interfaces;
using MyApi.Models;
using MyApi.Services;
using Microsoft.Extensions.Azure;
using Azure.Storage.Queues;
using Azure.Storage.Blobs;
using Azure.Core.Extensions;
using System;
namespace MyApi
{
public class Startup
{
public IConfiguration Configuration { get; }
private readonly ILogger<Startup> _logger;
private readonly IConfiguration _config;
public Startup(ILogger<Startup> logger, IConfiguration config)
{
_logger = logger;
_config = config;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add dBContext for DB
services.AddDbContextPool<MyApiDbContext>(options => options.UseSqlServer(_config.GetConnectionString("MyAzureDb")));
// Add DI Reference for Repository
services.AddScoped<IAuthRepository, AuthRepository>();
// Add DI Reference for Azure Blob Storage Processes
services.AddScoped<IBlobService, AzureBlobService>();
// DI Reference for AuthService
services.AddScoped<IAuthService, AuthService>();
// Add configuration section for Constructor Injection
services.Configure<ApiBlobStorageOptions>(_config.GetSection("MyApiBlobStorage"));
services.AddMvc(mvcOptions => mvcOptions.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Latest);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII
.GetBytes(_config.GetSection("MyApiBlobStorage:Secret").Value)),
ValidateIssuer = false,
ValidateAudience = false
};
options.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = context =>
{
_logger.LogWarning("Token authentication failed whilst attempting to upload file");
return Task.CompletedTask;
}
};
});
services.AddAzureClients(builder =>
{
builder.AddBlobServiceClient(Configuration["ConnectionStrings:MyApiBlobStorage/AzureBlobStorageConnectionString:blob"], preferMsi: true);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCors(x => x.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseAuthentication();
app.UseMvc();
}
}
internal static class StartupExtensions
{
public static IAzureClientBuilder<BlobServiceClient, BlobClientOptions> AddBlobServiceClient(this AzureClientFactoryBuilder builder, string serviceUriOrConnectionString, bool preferMsi)
{
if (preferMsi && Uri.TryCreate(serviceUriOrConnectionString, UriKind.Absolute, out Uri serviceUri))
{
return builder.AddBlobServiceClient(serviceUri);
}
else
{
return builder.AddBlobServiceClient(serviceUriOrConnectionString);
}
}
public static IAzureClientBuilder<QueueServiceClient, QueueClientOptions> AddQueueServiceClient(this AzureClientFactoryBuilder builder, string serviceUriOrConnectionString, bool preferMsi)
{
if (preferMsi && Uri.TryCreate(serviceUriOrConnectionString, UriKind.Absolute, out Uri serviceUri))
{
return builder.AddQueueServiceClient(serviceUri);
}
else
{
return builder.AddQueueServiceClient(serviceUriOrConnectionString);
}
}
}
}
Let me know if there is anything else required for understanding: the only difference between before and now is the addition of ExtraParam and the corresponding references throughout for the API, and the DB getting the identically named column.
I tried adding the parameter and deploying it to Azure and making the POST request as normal, starting and stopping the app service, deploying the API while the app service was stopped and starting it again, and restarting the app service. I don't know how much I could try changing up what I'm doing, I'm trying to do exactly the same as before, but with an extra parameter getting requested from the DB.
I can also confirm that the DB contains the ExtraParam column, and that it contains values against the existing data rows, as viewed using the Azure Portal's DB Query Editor.
I've resolved the issue, partially because of posting this question and sanitising the code for public discussion.
In the Login Controller, in my development code the request for the user to be returned was subsequently ignored, passing through the user request details which had a null ExtraParam, not the returned user which had the ExtraParam populated.
The moral of the story is to confirm which objects are being used at which points in the code, or have one object that is passed into, updated by, then returned from functions to maintain consistency.

Store token in httpcontext and use the same token in another request

I have a ASP.NET Core web application that hits a webservice endpoint and then that webservice sends requests to my application.
When I hit that webservice endpoint I receive a web token with a string value and expiry time. I am saving it in HttpContext to use it later.
When the webservice sends request to my application it sends the same token that I have received.
I need to make sure to validate that the token as it is the same that I initially received on my first request.
I do not want to store this token in my database because obviously I had to search the list of tokens and I can use a different token and as soon as it exists in the database this will work.
I have tried to store the token in HttpContext.Items
however, on request from the service to my app the token is gone. The Items have no token because I suspect it is a different Httpcontext.
On ASP .NET Framework I could store it as
HttpContext.Application["WebServiceToken"] = token;
However, I cannot find such an alternative on ASP .NET Core.
public async Task<IActionResult> Index()
{
if (serviceTokenService.TokenAlreadyExists())
{
return ArrivalsFromDatabase();
}
var exampleDate = new DateTime(2016, 3, 10);
var callback = Url.Action("ReceiveArrivalInfoFromService", "Home", null, Request.Scheme);
bool success = false;
var token = await this.serviceTokenService.GetServiceToken(configuration["WebServiceUrl"], exampleDate, callback);
if (!String.IsNullOrEmpty(token.Token))
{
this.serviceTokenService.SavesToken(token);
success = true;
}
if (!success)
{
return View("Error");
}
return ArrivalsFromDatabase();
}
public async Task<IActionResult> ReceiveArrivalInfoFromService()
{
var serviceToken = serviceTokenService.ReadToken();
var isTokenValid = serviceTokenService.ValidateToken(Request, serviceToken);
if (isTokenValid)
{
var arrivals = serviceTokenService.CollectArrivals(Request);
await arrivalService.AddRangeAsync(arrivals);
}
And this is my Servicetoken service where also my token method is.
public class ServiceTokenService : IServiceTokenService
{
private readonly IHttpContextAccessor httpContextAccessor;
public ServiceTokenService(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public void SavesToken(ServiceToken token)
{
httpContextAccessor.HttpContext.Items["ServiceToken"] = token;
}
public ServiceToken ReadToken()
{
if (httpContextAccessor.HttpContext.Items["ServiceToken"] != null)
{
return httpContextAccessor.HttpContext.Items["ServiceToken"] as ServiceToken;
}
return null;
}
}
On ReadToken it returns null. The token that was received previously is lost because it does not exists in HttpContext.items.
Here is an example of storing a value in your case the ServiceToken using a Session and appending it to each request using Middleware. Personally I would use cookie authorization or better yet IMO a JWT, however to help get you started relating to your question, something like this should help get you started.
First - Create The Middleware.
I normally like to create a folder Middleware just so its nice and neat in the solution and then right click on the newly created folder > Add > New Item > Middleware Class and name your new middleware file. (ServiceToken)
Add add the following code, this will check the session to see if there is a key called ServiceToken if there is, it will add the ServiceToken value to the httpContext Request Header.
public Task Invoke(HttpContext httpContext)
{
// Add Token To Context If Found.
string token = httpContext.Session.GetString("ServiceToken");
// Check Something Was Found.
if (!string.IsNullOrEmpty(token))
{
// Add The Custom Token Here.
httpContext.Request.Headers.TryAdd("ServiceToken", token);
}
// Next.
return _next(httpContext);
}
Second - Create The Session/MiddleWare, This is done in the Startup.cs file.
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.AddControllersWithViews();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
// Add Session And Middleware Here.
app.UseSession();
app.UseServiceToken();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Third - Add the token to the service.
In your case your'e adding it within the index endpoint.Something like this should get it started.
public IActionResult Index()
{
// Get Token.
string fakeToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
// Create Sercice Not If Not Already There.
if (!this.HttpContext.Session.TryGetValue("ServiceToken", out byte[] value))
{
this.HttpContext.Session.SetString("ServiceToken", fakeToken);
}
else
{
// Get The Token.
string token = Encoding.UTF8.GetString(value);
// TODO: Validate Here Or In The Middleware.
}
return View();
}
Note - As you're creating the ServiceToken from index, the first time the pipeline is completed, there will be no token attached to the request, as it is adding if available before the endpoint is called, this means that it will be added to the request once the first cycle is complete.

POST model binding isn't working with Namespace Routing Convention with .net core c#

I've been trying to use Namespace routing to build some APIs dynamically without the need to worry about hardcoding the routes. However, I did find an example from MSDN to use namespaces and folder structure as your API structure. Here's the sample that I have to use Namespace routing:
public class NamespaceRoutingConvention : Attribute, IControllerModelConvention
{
private readonly string _baseNamespace;
public NamespaceRoutingConvention(string baseNamespace)
{
_baseNamespace = baseNamespace;
}
public void Apply(ControllerModel controller)
{
var hasRouteAttributes = controller.Selectors.Any(selector => selector.AttributeRouteModel != null);
if (hasRouteAttributes)
{
return;
}
var namespc = controller.ControllerType.Namespace;
if (namespc == null) return;
var templateParts = new StringBuilder();
templateParts.Append(namespc, _baseNamespace.Length + 1, namespc.Length - _baseNamespace.Length - 1);
templateParts.Replace('.', '/');
templateParts.Append("/[controller]/[action]/{environment}/{version}");
var template = templateParts.ToString();
foreach (var selector in controller.Selectors)
{
selector.AttributeRouteModel = new AttributeRouteModel()
{
Template = template
};
}
}
}
And here's the controller:
namespace Backend.Controllers.Api.Project.Core
{
public class UserController : ApiBaseController
{
public UserController()
{
}
[HttpPost]
public IActionResult Login(LoginInput loginInput) // <-- loginInput properties return null
{
if (!ModelState.IsValid) return BadRequest();
return Ok(user);
}
}
}
in Startup.cs
namespace Backend
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Let's use namespaces as the routing default way for our APIs
services.AddControllers(options =>
{
options.Conventions.Add(new NamespaceRoutingConvention(typeof(Startup).Namespace + ".Controllers"));
});
}
}
}
Everything works ok except that when I trigger a POST api call to Login action the LoginInput doesn't get populated the values I'm sending through Postman i.e. {"username": "value", "password": "sample"} and it always returns null value. I'm not sure what am I doing wrong with the NamespaceRoutingConvention. Bear in mind if I remove it and hard-code the route in the controller like:
[ApiController]
[Route("api/project/core/[controller]/[action]/proda/v1")]
It works as expected. Any ideas?
Try to use this instead:
[HttpPost]
public IActionResult Login([FromBody]LoginInput loginInput)
{
if (!ModelState.IsValid) return BadRequest();
return Ok(user);
}
I think that by setting AttributeRouteModel, you're preventing the middleware invoked by having ApiControllerAttribute in the Controller to do its job, and so the defaults of treating object parameters as body is not applied.
This is a guess though, I haven't been able to find the corresponding code in the source code.

How to handle dynamic error pages in .net MVC Core?

Currently I have
app.UseExceptionHandler("/Home/Error");
I want to make the path relative to the original path.
For example if
Tenant1/PageThatThrowsError then
app.UseExceptionHandler("Tenant1/Home/Error");
but if
Tenant2/PageThatThrowsError then
app.UseExceptionHandler("Tenant2/Home/Error");
I thought I would be able to do
app.UseExceptionHandler(
new ExceptionHandlerOptions
{
ExceptionHandler = async (ctx) =>
{
//logic that extracts tenant
ctx.Request.Path = new PathString(Invariant($"{tenant}/Home/Error"));
}
}
);
but this throws a 500
EDIT: All the current solutions that for example uses redirects loses the current error context and does not allow the controller to for example call HttpContext.Features.Get().
We suppose that the application has required routes and endpoints of /Tenant1/Home/Error and /Tenant2/Home/Error. You can solve the issue using this code:
app.UseExceptionHandler(
new ExceptionHandlerOptions
{
ExceptionHandler = async (ctx) =>
{
string tenant = ctx.Request.Host.Value.Split('/')[0];
ctx.Response.Redirect($"/{tenant}/Home/Error");
},
}
);
Another equivalent solution is putting the following code on the startup.cs:
app.UseExceptionHandler("$/{tenant}/Home/Error");
We suppose that tenant comes from somewhere like appsettings. Then you can easily get exceptions on your desired endpoint by writing a simple route on your action:
[Route("/{TenantId}/Home/Error")]
public IActionResult Error(string TenantId)
{
string Id = TenantId;
// Here you can write your logic and decide what to do based on TenantId
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
or you can create two different actions:
[Route("/Tenant1/Home/Error")]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
[Route("/Tenant2/Home/Error")]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
Update:
If your tenants are dynamically added and can't be put in your appsettings.json (what we've supposed in the above solutions) you can write a middle-ware to handle the Exceptions, here is how:
Add the middle-ware in your Startup.cs in Configure method:
app.UseMiddleware(typeof(ErrorHandlingMiddleware));
At the next line add a route for errors (exactly after the middle-ware):
app.UseMvc(routes =>
{
routes.MapRoute(
name: "errors",
template: "{tenant}/{controller=Home}/{action=Index}/");
});
Create a class for your middle-ware, and put these code on:
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex,this.next);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception ex, RequestDelegate next)
{
string tenant = "tenant1";//write your logic something like this: context.Request.Path.Value.Split('/')[0];
context.Request.Path = new PathString($"/{tenant}/Home/Error");
context.Request.HttpContext.Features.Set<Exception>(ex);// add any object you want to the context
return next.Invoke(context);
}
}
Note that you can add anything you want to the context like this: context.Request.HttpContext.Features.Set<Exception>(ex);.
And finally you should create an action with an appropriate routing to write your logic there:
[Route("/{TenantId}/Home/Error")]
public IActionResult Error(string TenantId)
{
string Id = TenantId;
var exception= HttpContext.Features.Get<Exception>();// you can get the object which was set on the middle-ware
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
Note that the object which was set on the middle-ware, now can be retrieved.

Redirecting responsibility from short to full URL in another controller

As a homework I have to do a simple URL shortener, where I can add full link to list, which is processed by Hashids.net library, and I get short version of an URL.
I've got something like this now, but I got stuck on redirecting it back to full link.
I would like to add a new controller, which will take the responsibility of redirecting short URL to full URL. After clicking short URL it should go to localhost:xxxx/ShortenedUrl and then redirect to full link. Any tips how can I create this?
I was trying to do it by #Html.ActionLink(#item.ShortenedLink, "Index", "Redirect") and return Redirect(fullLink) in Redirect controller but it didn't work as I expect.
And one more question about routes, how can I achieve that after clicking short URL it will give me localhost:XXXX/ShortenedURL (i.e. localhost:XXXX/FSIAOFJO2#). Now I've got
#Html.DisplayFor(model => item.ShortenedLink)
and
app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller=Link}/{action=Index}");
});
but it gives me localhost:XXXX/Link/ShortenedURL, so I would like to omit this Link in URL.
View (part with Short URL):
<td>#Html.ActionLink(item.ShortenedLink,"GoToFull","Redirect", new { target = "_blank" }))</td>
Link controller:
public class LinkController : Controller
{
private ILinksRepository _repository;
public LinkController(ILinksRepository linksRepository)
{
_repository = linksRepository;
}
[HttpGet]
public IActionResult Index()
{
var links = _repository.GetLinks();
return View(links);
}
[HttpPost]
public IActionResult Create(Link link)
{
_repository.AddLink(link);
return Redirect("Index");
}
[HttpGet]
public IActionResult Delete(Link link)
{
_repository.DeleteLink(link);
return Redirect("Index");
}
}
Redirect controller which I am trying to do:
private ILinksRepository _repository;
public RedirectController(ILinksRepository linksRepository)
{
_repository = linksRepository;
}
public IActionResult GoToFull()
{
var links = _repository.GetLinks();
return Redirect(links[0].FullLink);
}
Is there a better way to get access to links list in Redirect Controller?
This is my suggestion, trigger the link via AJAX, here is working example:
This is the HTML element binded through model:
#Html.ActionLink(Model.ShortenedLink, "", "", null,
new { onclick = "fncTrigger('" + "http://www.google.com" + "');" })
This is the javascript ajax code:
function fncTrigger(id) {
$.ajax({
url: '#Url.Action("TestDirect", "Home")',
type: "GET",
data: { id: id },
success: function (e) {
},
error: function (err) {
alert(err);
},
});
}
Then on your controller to receive the ajax click:
public ActionResult TestDirect(string id)
{
return JavaScript("window.location = '" + id + "'");
}
Basically what I am doing here is that, after I click the link, it will call the TestDirect action, then redirect it to using the passed url parameter. You can do the conversion inside this action.
To create dynamic data-driven URLs, you need to create a custom IRouter. Here is how it can be done:
CachedRoute<TPrimaryKey>
This is a reusable generic class that maps a set of dynamically provided URLs to a single action method. You can inject an ICachedRouteDataProvider<TPrimaryKey> to provide the data (a URL to primary key mapping).
The data is cached to prevent multiple simultaneous requests from overloading the database (routes run on every request). The default cache time is for 15 minutes, but you can adjust as necessary for your requirements.
If you want it to act "immediate", you could build a more advanced cache that is updated just after a successful database update of one of the records. That is, the same action method would update both the database and the cache.
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class CachedRoute<TPrimaryKey> : IRouter
{
private readonly string _controller;
private readonly string _action;
private readonly ICachedRouteDataProvider<TPrimaryKey> _dataProvider;
private readonly IMemoryCache _cache;
private readonly IRouter _target;
private readonly string _cacheKey;
private object _lock = new object();
public CachedRoute(
string controller,
string action,
ICachedRouteDataProvider<TPrimaryKey> dataProvider,
IMemoryCache cache,
IRouter target)
{
if (string.IsNullOrWhiteSpace(controller))
throw new ArgumentNullException("controller");
if (string.IsNullOrWhiteSpace(action))
throw new ArgumentNullException("action");
if (dataProvider == null)
throw new ArgumentNullException("dataProvider");
if (cache == null)
throw new ArgumentNullException("cache");
if (target == null)
throw new ArgumentNullException("target");
_controller = controller;
_action = action;
_dataProvider = dataProvider;
_cache = cache;
_target = target;
// Set Defaults
CacheTimeoutInSeconds = 900;
_cacheKey = "__" + this.GetType().Name + "_GetPageList_" + _controller + "_" + _action;
}
public int CacheTimeoutInSeconds { get; set; }
public async Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
{
// Trim the leading slash
requestPath = requestPath.Substring(1);
}
// Get the page id that matches.
TPrimaryKey id;
//If this returns false, that means the URI did not match
if (!GetPageList().TryGetValue(requestPath, out id))
{
return;
}
//Invoke MVC controller/action
var routeData = context.RouteData;
// TODO: You might want to use the page object (from the database) to
// get both the controller and action, and possibly even an area.
// Alternatively, you could create a route for each table and hard-code
// this information.
routeData.Values["controller"] = _controller;
routeData.Values["action"] = _action;
// This will be the primary key of the database row.
// It might be an integer or a GUID.
routeData.Values["id"] = id;
await _target.RouteAsync(context);
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
VirtualPathData result = null;
string virtualPath;
if (TryFindMatch(GetPageList(), context.Values, out virtualPath))
{
result = new VirtualPathData(this, virtualPath);
}
return result;
}
private bool TryFindMatch(IDictionary<string, TPrimaryKey> pages, IDictionary<string, object> values, out string virtualPath)
{
virtualPath = string.Empty;
TPrimaryKey id;
object idObj;
object controller;
object action;
if (!values.TryGetValue("id", out idObj))
{
return false;
}
id = SafeConvert<TPrimaryKey>(idObj);
values.TryGetValue("controller", out controller);
values.TryGetValue("action", out action);
// The logic here should be the inverse of the logic in
// RouteAsync(). So, we match the same controller, action, and id.
// If we had additional route values there, we would take them all
// into consideration during this step.
if (action.Equals(_action) && controller.Equals(_controller))
{
// The 'OrDefault' case returns the default value of the type you're
// iterating over. For value types, it will be a new instance of that type.
// Since KeyValuePair<TKey, TValue> is a value type (i.e. a struct),
// the 'OrDefault' case will not result in a null-reference exception.
// Since TKey here is string, the .Key of that new instance will be null.
virtualPath = pages.FirstOrDefault(x => x.Value.Equals(id)).Key;
if (!string.IsNullOrEmpty(virtualPath))
{
return true;
}
}
return false;
}
private IDictionary<string, TPrimaryKey> GetPageList()
{
IDictionary<string, TPrimaryKey> pages;
if (!_cache.TryGetValue(_cacheKey, out pages))
{
// Only allow one thread to poplate the data
lock (_lock)
{
if (!_cache.TryGetValue(_cacheKey, out pages))
{
pages = _dataProvider.GetPageToIdMap();
_cache.Set(_cacheKey, pages,
new MemoryCacheEntryOptions()
{
Priority = CacheItemPriority.NeverRemove,
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(this.CacheTimeoutInSeconds)
});
}
}
}
return pages;
}
private static T SafeConvert<T>(object obj)
{
if (typeof(T).Equals(typeof(Guid)))
{
if (obj.GetType() == typeof(string))
{
return (T)(object)new Guid(obj.ToString());
}
return (T)(object)Guid.Empty;
}
return (T)Convert.ChangeType(obj, typeof(T));
}
}
LinkCachedRouteDataProvider
Here we have a simple service that retrieves the data from the database and loads it into a Dictionary. The most complicated part is the scope that needs to be setup in order to use DbContext from within the service.
public interface ICachedRouteDataProvider<TPrimaryKey>
{
IDictionary<string, TPrimaryKey> GetPageToIdMap();
}
public class LinkCachedRouteDataProvider : ICachedRouteDataProvider<int>
{
private readonly IServiceProvider serviceProvider;
public LinkCachedRouteDataProvider(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider
?? throw new ArgumentNullException(nameof(serviceProvider));
}
public IDictionary<string, int> GetPageToIdMap()
{
using (var scope = serviceProvider.CreateScope())
{
var dbContext = scope.ServiceProvider.GetService<ApplicationDbContext>();
return (from link in dbContext.Links
select new KeyValuePair<string, int>(
link.ShortenedLink.Trim('/'),
link.Id)
).ToDictionary(pair => pair.Key, pair => pair.Value);
}
}
}
RedirectController
Our redirect controller accepts the primary key as an id parameter and then looks up the database record to get the URL to redirect to.
public class RedirectController
{
private readonly ApplicationDbContext dbContext;
public RedirectController(ApplicationDbContext dbContext)
{
this.dbContext = dbContext
?? throw new ArgumentNullException(nameof(dbContext));
}
public IActionResult GoToFull(int id)
{
var link = dbContext.Links.FirstOrDefault(x => x.Id == id);
return new RedirectResult(link.FullLink);
}
}
In a production scenario, you would probably want to make this a permanent redirect return new RedirectResult(link.FullLink, true), but those are automatically cached by browsers which makes testing difficult.
Startup.cs
We setup the DbContext, the memory cache, and the LinkCachedRouteDataProvider in our DI container for use later.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddMvc();
services.AddMemoryCache();
services.AddSingleton<LinkCachedRouteDataProvider>();
}
And then we setup our routing using the CachedRoute<TPrimaryKey>, providing all dependencies.
app.UseMvc(routes =>
{
routes.Routes.Add(new CachedRoute<int>(
controller: "Redirect",
action: "GoToFull",
dataProvider: app.ApplicationServices.GetService<LinkCachedRouteDataProvider>(),
cache: app.ApplicationServices.GetService<IMemoryCache>(),
target: routes.DefaultHandler)
// Set to 60 seconds of caching to make DB updates refresh quicker
{ CacheTimeoutInSeconds = 60 });
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
To build these short URLs on the user interface, you can use tag helpers (or HTML helpers) the same way you would with any other route:
<a asp-area="" asp-controller="Redirect" asp-action="GoToFull" asp-route-id="1">
#Url.Action("GoToFull", "Redirect", new { id = 1 })
</a>
Which is generated as:
/M81J1w0A
You can of course use a model to pass the id parameter into your view when it is generated.
<a asp-area="" asp-controller="Redirect" asp-action="GoToFull" asp-route-id="#Model.Id">
#Url.Action("GoToFull", "Redirect", new { id = Model.Id })
</a>
I have made a Demo on GitHub. If you enter the short URLs into the browser, they will be redirected to the long URLs.
M81J1w0A -> https://maps.google.com/
r33NW8K -> https://stackoverflow.com/
I didn't create any of the views to update the URLs in the database, but that type of thing is covered in several tutorials such as Get started with ASP.NET Core MVC and Entity Framework Core using Visual Studio, and it doesn't look like you are having issues with that part.
References:
Get started with ASP.NET Core MVC and Entity Framework Core using Visual Studio
Change route collection of MVC6 after startup
MVC Routing template to represent infinite self-referential hierarchical category structure
Imlementing a Custom IRouter in ASP.NET 5 (vNext) MVC 6

Categories

Resources