I recently started developing a Blazor WebAssembly app, and now I'm settling on a database connection.
All lessons and instructions say that you need to enter information into the Startup.cs file and appsettings.json, but these files are not in the project.
I do not understand. In Blazor WebAssembly, is there no way to connect to the DB?
Not directly. Blazor WebAssembly is a front end framework. You need to create an API controller to wrap your database connection and use HttpClient to call the api. A straight forward way to do it is to use an asp.net core web api controller wrapping an Entity Framework Core Database context.
#inject HttpClient Http
<template_html_here/>
#code
{
protected override async Task OnInitializedAsync()
{
products = await Http.GetFromJsonAsync<Product[]>("api/Products");
}
}
Controller:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly ProductsDbContext _context; // your database context
public ProductsController(ProductsDbContext context)
{
_context = context;
}
[HttpGet]
public IEnumerable<Product> Get()
{
return _context.Products.ToList();
}
}
You can read more about blazor at https://learn.microsoft.com/en-us/aspnet/core/blazor/call-web-api?view=aspnetcore-3.1.
And on asp.net core web APIs on https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-web-api?view=aspnetcore-3.1&tabs=visual-studio.
It is now 2022. .NET 6 has shipped, and Blazor WebAssembly has support for compiled binaries.
That means there are now three options for using a database in a Blazor WebAssembly application.
#1. Create a webApi. Call the webApi on from the client as you can see being done in the default sample. See FetchData.razor
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}
and WeatherForecastController.cs on the server. The default solution does not call a database, but you could easily use a dbContext in Get() to pull data from a database.
#2: With support for compiled binaries in Blazor WebAssembly, it is now possible to completely host Sqlite in WebAssembly.
https://github.com/TrevorDArcyEvans/BlazorSQLiteWasm
#3: IndexedDb. Through js interop, the IndexDb in the browser can be used. Large amounts of data can be stored in this Db, and like the name implies, it is indexed. Since this can be accidentally cleared, it is most useful in a PWA, where that is more difficult. Also, with this and Sqlite, anything done in the browser is open to the user and hackers that compromise the user's maching.
I use https://github.com/wtulloch/Blazor.IndexedDB
You add schema in program.cs:
builder.Services.AddIndexedDB(dbStore =>
{
dbStore.DbName = "SomeDbName";
dbStore.Version = 1;
dbStore.Stores.Add(new StoreSchema
{
Name = "People",
PrimaryKey = new IndexSpec { Name = "id", KeyPath = "id", Auto = false },
Indexes = new List<IndexSpec>
{
new IndexSpec{Name="alias", KeyPath = "alias", Auto=false},
new IndexSpec{Name="isAvailable", KeyPath = "isAvailable", Auto=false},
new IndexSpec{Name="communityId", KeyPath = "communityId", Auto=false},
new IndexSpec{Name="isFriend", KeyPath = "isFriend", Auto=false},
}
});
});
In this code, the names of the fields are camelCased, whereas the objects I'm constructing are PascalCase. This was actually necessary for me to get it to work. I think my serializer may be set to camelCase Json or something, so watch that.
And then you add remove and search using:
public async Task AddPersonAsync(LocalPerson member)
{
var newPerson = new StoreRecord<LocalPerson>
{
Data = member,
Storename = PeopleStoreName
};
await _dbManager.AddRecord(newPerson);
}
public async Task<LocalPerson> GetPersonByIdAsync(Guid id)
{
var localPerson = await _dbManager.GetRecordById<Guid, LocalPerson>(PeopleStoreName, id);
return localPerson;
}
public async Task<List<LocalPerson>> GetPeopleAsync()
{
var results = await _dbManager.GetRecords<LocalPerson>(PeopleStoreName);
return results;
}
public async Task<List<LocalPerson>> GetPeopleByCommunityAsync(Guid id)
{
var indexSearch = new StoreIndexQuery<Guid>
{
Storename = PeopleStoreName,
IndexName = "communityId",
QueryValue = id,
};
var result = await _dbManager.GetAllRecordsByIndex<Guid, LocalPerson>(indexSearch);
if (result is null)
{
return new List<LocalPerson>();
}
return (List<LocalPerson>)result;
}
If you are referring to local storage (browser storage) then this component by Chris Sainty could help you.
However if you are looking for a connection to a Database like a SQL Server or document storage like Mongo it can not be done directly.
Blazor Wasm is for front end development. You will need to call web APIs that connect to databases stored on servers.
Related
I am writing a web app that I would like to have access to Sharepoint Document Libraries from a particular site using the currently logged on user credentials. I have looked at a number of articles that suggest using the PnP Framework and using a certificate instead of the client/secret ids.
I have tried both, the code of which is below:
string siteCollectionURL = "https://mycompanyname.sharepoint.com/sites/staffportal";
var authManager = new AuthenticationManager(ApplicationId, "C:\\pathtopfxfile\certifcatefx.pfx", "certificatepassword", "https://mycompany.sharepoint.com/sites/staffportal");
using (var clientContext = authManager.GetACSAppOnlyContext(siteCollectionURL,ApplicationId,ClientSecretId))
{
clientContext.Load(clientContext.Web, p => p.Title);
clientContext.ExecuteQuery();
return Ok(clientContext.Web.Title);
}
Unfortunately on the ExecuteQuery line I am consistently getting the 401 error, indicating that I am not authorized.
I registered the app in Azure -> Enterprise applications:
I have checked the following articles:
How to use the PnP Framework in a c# application
Secure Authentication of SharePoint with PnP Framework with C#(Code)
And tried the code snippets, but I cannot seem to find anything that suggests using the currently logged in user to the Web app.(see screen shot) - the user is a global administrator
Below is the error:
Any suggestions would be much appreciated.
Regards,
Darren
UPDATE TO ARTICLE
jimas13's link is what pointed me in the right direction. The tweaks I mentioned in the comment I have posted below for anyone wanting to write an MVC C# web app. This does require that the app needs to be registered and needs a self-signed certificate setup.
The two Async lines need to be written as follows:
public static async Task<AuthenticationResult> GetToken(IConfidentialClientApplication app, string[] scopes)
{
return await app.AcquireTokenForClient(scopes).ExecuteAsync();
}
public static async Task<Web> GetClientContext(string Uri,AuthenticationResult authResult)
{
using (var clientContext = ContextHelper.GetClientContext(Uri, authResult.AccessToken))
{
Web web = clientContext.Web;
clientContext.Load(web);
await clientContext.ExecuteQueryAsync();
return web;
}
}
The rest of the code is here:
public IActionResult Index()
{
AuthenticationConfiguration.AuthenticationConfiguration config = AuthenticationConfiguration.AuthenticationConfiguration.ReadFromJsonFile("appsettings.json");
string siteURL = config.SiteUrl;
string[] scopes = new string[] { config.Scope };
CertificateDescription certificate = config.Certificate;
ICertificateLoader certificateLoader = new DefaultCertificateLoader();
certificateLoader.LoadIfNeeded(certificate);
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithCertificate(certificate.Certificate)
.WithTenantId(config.Tenant)
.WithAuthority(config.Authority)
.Build();
AuthenticationResult result = GetToken(app, scopes).Result;
Web WebSite = GetClientContext(siteURL, result).Result;
return Ok(WebSite.Title);
}
The .SiteUrl and .Scope were added to the AuthenticationConfiguration.cs file as a property and then also added to the appsettings.json file.
I have been trying to deploy my asp.net core hosted blazor webassembly app on Azure App Service, however, I am having trouble getting the api to work. When I try and save a user's data in the database, I get a 400 bad request error. It works fine on localhost. I looked around and found advice that suggested that I use the Log Stream in Azure to get a more detailed error message, and here it is although I'm not sure the details really help.
2020-06-22 22:24:54 MYPROJECT POST /api/Register/Post X-ARR-LOG-ID=ef27263e-dead-417a-b136-89a217a6f931 443 - MYIP Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/83.0.4103.97+Safari/537.36 ARRAffinity=7f74b113b9ae575a087ae1fa07a63858e6e34f27224b7aa1a957b06074e65ffd https://myproject.azurewebsites.net/Register
Here is the relevant application code:
//Register.razor in client project
#code {
private RegisterModel Model = new RegisterModel();
private bool ShowErrors;
private List<string> Errors = new List<string>();
private async Task HandleValidSubmit()
{
ShowErrors = false;
Errors.Clear();
if (Model.Password.Length >= 6 && Model.Password == Model.ConfirmPassword)
{
await HttpClient.PostAsJsonAsync<RegisterModel>("api/Register/Post", Model);
NavigationManager.NavigateTo("/");
}
else
{
if (Model.Password.Length > 100 || Model.Password.Length < 6)
{
Errors.Add("Password must be between 6 and 100 characters in length.");
}
if (Model.Password != Model.ConfirmPassword)
{
Errors.Add("Passwords do not match.");
}
ShowErrors = true;
}
}
}
//RegisterController.cs in server project
[Route("api/Register")]
public class RegisterController : Controller
{
private UserContext UserContext { get; set; }
private IHasher Hasher = new Pbkdf2Hasher();
public RegisterController (UserContext userContext)
{
UserContext = userContext;
}
[RequireHttps]
[HttpPost]
[Route("Post")]
public async Task Post([FromBody]RegisterModel model)
{
var user = new UserModel
{
Email = model.Email,
Password = Hasher.Hash(model.Password)
};
await UserContext.AddAsync(user);
await UserContext.SaveChangesAsync();
}
}
//Startup.cs in Server project
public void ConfigureServices(IServiceCollection services)
services.AddDbContext<UserContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("UsersConnection"),
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.EnableRetryOnFailure();
}));
On publishing the project, I configured an Azure SQL Database for 'users', checked the checkboxes to use the UsersConnection string at runtime, and apply the UserContext Entity Framework Migration on publish.
I am using visual studio 2019, and the target framework is netcoreapp3.1. I'd appreciate any guidance. Thanks!
Edit
After looking at the detailed logs, apparently the database isn't even being made?
INSERT INTO [Users] ([Email], [Password])
VALUES (#p0, #p1);
2020-06-22 22:19:47.208 +00:00 [Error] Microsoft.EntityFrameworkCore.Update: An exception occurred in the database while saving changes for context type 'BlazorTodos.Server.Data.UserContext'.
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
---> Microsoft.Data.SqlClient.SqlException (0x80131904): Invalid object name 'Users'.
Thanks for all the suggestions, Jason. Helped me head in the right direction.
The problem was quite silly. Apparently the Database and Entity Framework settings had automatically come up with the same string for both my "Users" and "Todos" databases:
Data Source=tcp:myserver.database.windows.net,1433;Initial Catalog=todos;User Id=<myid>#myserver;Password=<my-password>
But I hadn't noticed that it was the same string. I just had to change Initial Catalog to users and re check the "use this connection string at runtime" and "apply this migration on publish" boxes again with the edited string. That solved the issue.
Background
I have a web api server (asp.net core v2.1) that serve some basic operation, like managing entities on the server. This is the interface:
[HttpPost]
[Route("create")]
public async Task<ActionResult<NewEntityResponse>> Create(CreateEntityModel model)
{
// 1) Validate the request.
// 2) Create a new row on the database
// 3) Return the new entity in response.
}
The user running this REST method in this way:
POST https://example.com/create
Content-Type: application/json
{
"firstName": "Michael",
"lastName": "Jorden"
}
And getting response like this:
Status 200
{
"id": "123456" // The newly created entity id
}
The Problem
When sending thousands of requests like this, at some point it will fail because of network connections. When connection fails, it can leads us into two different situations:
The network call was ended on the way to the server - In this case, the server don't know about this request. Therefore, the entity wasn't created. The user just have to send the same message again.
The network call was sent from the server to back to the client but never rich the destination - In this case the request was fulfill completely, but the client don't aware for this. The expected solution is to send the same request again. In this case, it will create the same entity twice - and this is the problem.
The Requested Solution
I want to create an generic solution for web-api that "remmeber" which commands it already done. if he got same request twice, it's return HTTP status code Conflict.
Where I got so far
I thought to add the client an option to add a unique id to the request, in this way:
POST https://example.com/create?call-id=XXX
Add to my server a new filter that check if the key XXX is already fulfill. If yes, return Conflict. Otherwise - continue.
Add another server filter that checks the response of the method and marking it as "completed" for further checks.
The problem with this solution on concurrency calls. If my method takes 5 seconds to be returned and the client sent the same message again after 1 second - it will create two entities with same data.
The Questions:
Do you think that this is good approach to solve this problem?
Do you familiar with ready to use solutions that doing this?
How to solve my "concurrency" problem?
Any other tips will be great!
thanks.
Isnt the easiest solution to make the REST action idempotent?
I mean by that: the call should check if the resource already exists and either create a new resource if it doesnt or return the existing if it does?
OK, I just figure it up how to make it right. So, I implemented it by myself and share it with you.
In order to sync all requests between different servers, I used Redis as cache service. If you have only one server, you can use Dictionary<string, string> instead.
This filter do:
Before processing the request - add a new empty value key to Redis.
After the server processed the request - store the server response in Redis. This data will be used when the user will ask again for same request.
public class ConflictsFilter : ActionFilterAttribute
{
const string CONFLICT_KEY_NAME = "conflict-checker";
static readonly TimeSpan EXPIRE_AFTER = TimeSpan.FromMinutes(30);
private static bool ShouldCheck(ActionDescriptor actionDescription, IQueryCollection queries)
{
return queries.ContainsKey(CONFLICT_KEY_NAME);
}
private string BuildKey(string uid, string requestId)
{
return $"{uid}_{requestId}";
}
public override void OnActionExecuting(ActionExecutingContext context)
{
if (ShouldCheck(context.ActionDescriptor, context.HttpContext.Request.Query))
{
using (var client = RedisConnectionPool.ConnectionPool.GetClient())
{
string key = BuildKey(context.HttpContext.User.GetId(), context.HttpContext.Request.Query[CONFLICT_KEY_NAME]);
string existing = client.Get<string>(key);
if (existing != null)
{
var conflict = new ContentResult();
conflict.Content = existing;
conflict.ContentType = "application/json";
conflict.StatusCode = 409;
context.Result = conflict;
return;
}
else
{
client.Set(key, string.Empty, EXPIRE_AFTER);
}
}
}
base.OnActionExecuting(context);
}
public override void OnResultExecuted(ResultExecutedContext context)
{
base.OnResultExecuted(context);
if (ShouldCheck(context.ActionDescriptor, context.HttpContext.Request.Query) && context.HttpContext.Response.StatusCode == 200)
{
string key = BuildKey(context.HttpContext.User.GetId(), context.HttpContext.Request.Query[CONFLICT_KEY_NAME]);
using (var client = RedisConnectionPool.ConnectionPool.GetClient())
{
var responseBody = string.Empty;
if (context.Result is ObjectResult)
{
ObjectResult result = context.Result as ObjectResult;
responseBody = JsonConvert.SerializeObject(result.Value);
}
if (responseBody != string.Empty)
client.Set(key, responseBody, EXPIRE_AFTER);
}
}
}
}
The code is executed only if the query ?conflict-checker=XXX is exists.
This code is provide you under MIT license.
Enjoy the ride :)
I'm developing an ASP.Net Core web application where I need to create a kind of "authentication proxy" to another (external) web service.
What I mean by authentication proxy is that I will receive requests through a specific path of my web app and will have to check the headers of those requests for an authentication token that I'll have issued earlier, and then redirect all the requests with the same request string / content to an external web API which my app will authenticate with through HTTP Basic auth.
Here's the whole process in pseudo-code
Client requests a token by making a POST to a unique URL that I sent him earlier
My app sends him a unique token in response to this POST
Client makes a GET request to a specific URL of my app, say /extapi and adds the auth-token in the HTTP header
My app gets the request, checks that the auth-token is present and valid
My app does the same request to the external web API and authenticates the request using BASIC authentication
My app receives the result from the request and sends it back to the client
Here's what I have for now. It seems to be working fine, but I'm wondering if it's really the way this should be done or if there isn't a more elegant or better solution to this? Could that solution create issues in the long run for scaling the application?
[HttpGet]
public async Task GetStatement()
{
//TODO check for token presence and reject if issue
var queryString = Request.QueryString;
var response = await _httpClient.GetAsync(queryString.Value);
var content = await response.Content.ReadAsStringAsync();
Response.StatusCode = (int)response.StatusCode;
Response.ContentType = response.Content.Headers.ContentType.ToString();
Response.ContentLength = response.Content.Headers.ContentLength;
await Response.WriteAsync(content);
}
[HttpPost]
public async Task PostStatement()
{
using (var streamContent = new StreamContent(Request.Body))
{
//TODO check for token presence and reject if issue
var response = await _httpClient.PostAsync(string.Empty, streamContent);
var content = await response.Content.ReadAsStringAsync();
Response.StatusCode = (int)response.StatusCode;
Response.ContentType = response.Content.Headers.ContentType?.ToString();
Response.ContentLength = response.Content.Headers.ContentLength;
await Response.WriteAsync(content);
}
}
_httpClient being a HttpClient class instantiated somewhere else and being a singleton and with a BaseAddressof http://someexternalapp.com/api/
Also, is there a simpler approach for the token creation / token check than doing it manually?
If anyone is interested, I took the Microsoft.AspNetCore.Proxy code and made it a little better with middleware.
Check it out here: https://github.com/twitchax/AspNetCore.Proxy. NuGet here: https://www.nuget.org/packages/AspNetCore.Proxy/. Microsoft archived the other one mentioned in this post, and I plan on responding to any issues on this project.
Basically, it makes reverse proxying another web server a lot easier by allowing you to use attributes on methods that take a route with args and compute the proxied address.
[ProxyRoute("api/searchgoogle/{query}")]
public static Task<string> SearchGoogleProxy(string query)
{
// Get the proxied address.
return Task.FromResult($"https://www.google.com/search?q={query}");
}
I ended up implementing a proxy middleware inspired by a project in Asp.Net's GitHub.
It basically implements a middleware that reads the request received, creates a copy from it and sends it back to a configured service, reads the response from the service and sends it back to the caller.
This post talks about writing a simple HTTP proxy logic in C# or ASP.NET Core. And allowing your project to proxy the request to any other URL. It is not about deploying a proxy server for your ASP.NET Core project.
Add the following code anywhere of your project.
public static HttpRequestMessage CreateProxyHttpRequest(this HttpContext context, Uri uri)
{
var request = context.Request;
var requestMessage = new HttpRequestMessage();
var requestMethod = request.Method;
if (!HttpMethods.IsGet(requestMethod) &&
!HttpMethods.IsHead(requestMethod) &&
!HttpMethods.IsDelete(requestMethod) &&
!HttpMethods.IsTrace(requestMethod))
{
var streamContent = new StreamContent(request.Body);
requestMessage.Content = streamContent;
}
// Copy the request headers
foreach (var header in request.Headers)
{
if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null)
{
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
}
}
requestMessage.Headers.Host = uri.Authority;
requestMessage.RequestUri = uri;
requestMessage.Method = new HttpMethod(request.Method);
return requestMessage;
}
This method covert user sends HttpContext.Request to a reusable HttpRequestMessage. So you can send this message to the target server.
After your target server response, you need to copy the responded HttpResponseMessage to the HttpContext.Response so the user's browser just gets it.
public static async Task CopyProxyHttpResponse(this HttpContext context, HttpResponseMessage responseMessage)
{
if (responseMessage == null)
{
throw new ArgumentNullException(nameof(responseMessage));
}
var response = context.Response;
response.StatusCode = (int)responseMessage.StatusCode;
foreach (var header in responseMessage.Headers)
{
response.Headers[header.Key] = header.Value.ToArray();
}
foreach (var header in responseMessage.Content.Headers)
{
response.Headers[header.Key] = header.Value.ToArray();
}
// SendAsync removes chunking from the response. This removes the header so it doesn't expect a chunked response.
response.Headers.Remove("transfer-encoding");
using (var responseStream = await responseMessage.Content.ReadAsStreamAsync())
{
await responseStream.CopyToAsync(response.Body, _streamCopyBufferSize, context.RequestAborted);
}
}
And now the preparation is complete. Back to our controller:
private readonly HttpClient _client;
public YourController()
{
_client = new HttpClient(new HttpClientHandler()
{
AllowAutoRedirect = false
});
}
public async Task<IActionResult> Rewrite()
{
var request = HttpContext.CreateProxyHttpRequest(new Uri("https://www.google.com"));
var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);
await HttpContext.CopyProxyHttpResponse(response);
return new EmptyResult();
}
And try to access it. It will be proxied to google.com
A nice reverse proxy middleware implementation can also be found here: https://auth0.com/blog/building-a-reverse-proxy-in-dot-net-core/
Note that I replaced this line here
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
with
requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToString());
Original headers (e.g. like an authorization header with a bearer token) would not be added without my modification in my case.
I had luck using twitchax's AspNetCore.Proxy NuGet package, but could not get it to work using the ProxyRoute method shown in twitchax's answer. (Could have easily been a mistake on my end.)
Instead I defined the mapping in Statup.cs Configure() method similar to the code below.
app.UseProxy("api/someexternalapp-proxy/{arg1}", async (args) =>
{
string url = "https://someexternalapp.com/" + args["arg1"];
return await Task.FromResult<string>(url);
});
Piggy-backing on James Lawruk's answer https://stackoverflow.com/a/54149906/6596451 to get the twitchax Proxy attribute to work, I was also getting a 404 error until I specified the full route in the ProxyRoute attribute. I had my static route in a separate controller and the relative path from Controller's route was not working.
This worked:
public class ProxyController : Controller
{
[ProxyRoute("api/Proxy/{name}")]
public static Task<string> Get(string name)
{
return Task.FromResult($"http://www.google.com/");
}
}
This does not:
[Route("api/[controller]")]
public class ProxyController : Controller
{
[ProxyRoute("{name}")]
public static Task<string> Get(string name)
{
return Task.FromResult($"http://www.google.com/");
}
}
Hope this helps someone!
Twitchax's answer seems to be the best solution at the moment. In researching this, I found that Microsoft is developing a more robust solution that fits the exact problem the OP was trying to solve.
Repo: https://github.com/microsoft/reverse-proxy
Article for Preview 1 (they actually just released prev 2): https://devblogs.microsoft.com/dotnet/introducing-yarp-preview-1/
From the Article...
YARP is a project to create a reverse proxy server. It started when we noticed a pattern of questions from internal teams at Microsoft who were either building a reverse proxy for their service or had been asking about APIs and technology for building one, so we decided to get them all together to work on a common solution, which has become YARP.
YARP is a reverse proxy toolkit for building fast proxy servers in .NET using the infrastructure from ASP.NET and .NET. The key differentiator for YARP is that it is being designed to be easily customized and tweaked to match the specific needs of each deployment scenario. YARP plugs into the ASP.NET pipeline for handling incoming requests, and then has its own sub-pipeline for performing the steps to proxy the requests to backend servers. Customers can add additional modules, or replace stock modules as needed.
...
YARP works with either .NET Core 3.1 or .NET 5 preview 4 (or later). Download the preview 4 (or greater) of .NET 5 SDK from https://dotnet.microsoft.com/download/dotnet/5.0
More specifically, one of their sample apps implements authentication (as for the OP's original intent)
https://github.com/microsoft/reverse-proxy/blob/master/samples/ReverseProxy.Auth.Sample/Startup.cs
Here is a basic implementation of Proxy library for ASP.NET Core:
This does not implement the authorization but could be useful to someone looking for a simple reverse proxy with ASP.NET Core. We only use this for development stages.
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace Sample.Proxy
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(options =>
{
options.AddDebug();
options.AddConsole(console =>
{
console.IncludeScopes = true;
});
});
services.AddProxy(options =>
{
options.MessageHandler = new HttpClientHandler
{
AllowAutoRedirect = false,
UseCookies = true
};
options.PrepareRequest = (originalRequest, message) =>
{
var host = GetHeaderValue(originalRequest, "X-Forwarded-Host") ?? originalRequest.Host.Host;
var port = GetHeaderValue(originalRequest, "X-Forwarded-Port") ?? originalRequest.Host.Port.Value.ToString(CultureInfo.InvariantCulture);
var prefix = GetHeaderValue(originalRequest, "X-Forwarded-Prefix") ?? originalRequest.PathBase;
message.Headers.Add("X-Forwarded-Host", host);
if (!string.IsNullOrWhiteSpace(port)) message.Headers.Add("X-Forwarded-Port", port);
if (!string.IsNullOrWhiteSpace(prefix)) message.Headers.Add("X-Forwarded-Prefix", prefix);
return Task.FromResult(0);
};
});
}
private static string GetHeaderValue(HttpRequest request, string headerName)
{
return request.Headers.TryGetValue(headerName, out StringValues list) ? list.FirstOrDefault() : null;
}
public void Configure(IApplicationBuilder app)
{
app.UseWebSockets()
.Map("/api", api => api.RunProxy(new Uri("http://localhost:8833")))
.Map("/image", api => api.RunProxy(new Uri("http://localhost:8844")))
.Map("/admin", api => api.RunProxy(new Uri("http://localhost:8822")))
.RunProxy(new Uri("http://localhost:8811"));
}
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
}
}
I'm working on an ASP.NET Core 2 MVC application, trying my hand at setting up a service layer. I have a pretty basic application in which I want to:
upload an image alongside a name
add a record to the database
store the file on disk in the wwwroot folder with a naming convention that corresponds to the primary key of the record I just added.
Right now I've got that all that working just fine in my controller with a viewmodel. But I'd like to instead have a nice method in my service layer that does both the database add and the file system add, but I'm not finding a way to pass the file over to the service.
I figured out how to make the database add in the service layer too, but not how to get the file over there to do anything with it.
This is my controller:
[HttpPost]
public async Task<IActionResult> Create(CreateMentorViewModel model)
{
var mentor = new Mentor
{
FirstName = model.FirstName,
LastName = model.LastName,
Created = DateTime.Now,
};
_ctx.Add(mentor);
await _ctx.SaveChangesAsync();
int newid = mentor.Id;
if (model.Image != null)
{
var uploads = Path.Combine(_hostingEnvironment.WebRootPath, "img/profilepics");
var fileId = newid + ".jpg";
var filePath = Path.Combine(uploads, fileId);
model.Image.CopyTo(new FileStream(filePath, FileMode.Create));
}
return RedirectToAction("Index", "Mentor");
}
What I'm looking for is some pointers on how to send the file from the viewmodel in the controller to the service or maybe more to the point - should I do that at all?
You shouldn't depend on ASP.NET in your service layer. Pass on Stream instead:
var result = await myService.ProcessFileAsync(formFile.OpenReadStream());
You can simply pass the IFormFile as a parameter to the service e.g.
public void MyMethod(IFormFile file)
{
...
}
and pass model.Image
[HttpPost]
public async Task<IActionResult> Create(CreateMentorViewModel model)
{
...
myService.MyMethod(model.Image);
...
}