I can create a post. Also, it is available to add image in the post. But when I edit post later, the image is removed and I have to add this image again in each editing. How can I do that the image will be remembered for always and it will be changed only in case if i will be add new?
AdminController.cs (it's responsible for editing)
[HttpGet]
public IActionResult EditPost(int? id)
{
if (id == null)
{
return View(new PostViewModel());
}
else
{
var post = _repo.GetPost((int)id);
return View(new PostViewModel
{
Id = post.Id,
Text = post.Text,
});
}
}
[HttpPost]
public async Task<IActionResult> EditPost(PostViewModel vm)
{
var post = new Post
{
Id = vm.Id,
Text = vm.Text,
Image = await _fileManager.SaveImage(vm.Image)
};
if (post.Id == 0)
_repo.AddPost(post);
else
_repo.UpdatePost(post);
if (await _repo.SaveChangesAsync())
return RedirectToAction("Posts");
else
return View(post);
}
Post.cs in Models
namespace Project.Models
{
public class Post
{
public int Id { get; set; }
public string Text { get; set; } = "";
public string Image { get; set; } = "";
}
}
Repository.cs
public void UpdatePost(Post post)
{
_ctx.Post.Update(post);
}
IRepository.cs
void UpdatePost(Post post);
Just in case I publish PostController.cs
public IActionResult Index()
{
var posts = _repo.GetAllPosts();
return View(posts);
}
public IActionResult Post(int id)
{
var post= _repo.GetPost(id);
return View(post);
}
[HttpGet("/Image/{image}")]
public IActionResult Image(string image)
{
var path = image.Substring(image.LastIndexOf('.') + 1);
return new FileStreamResult(_fileManager.ImageStream(image), $"image/{path}");
}
This is expected behavior. The file upload cannot be set by the server, because the server does not have access to the client's filesystem. The client must always consent to uploading a local file, and that consent is given via the file upload dialog. If the client is sent back to the form due to a validation error, they must upload the file again.
Now, that said, there's workarounds, though none of them ideal. The file input can be manually set with file data (just not a filesystem reference) via the JavaScript File API. You could use the File API to read the file data after the user chooses a file, store that in localStorage (in the browser) and then attempt to read from localStorage on page load, and set the file input to that file data, if there's a key present.
However, localStorage has a hard limit of 10MB total, so you'd need to be careful to ensure that the file size does not exceed that, along with anything else that might already be stored or will be stored there, as well.
Alternatively, you can store the file server-side before you send the user back, so that it's still available after they correct their entries and re-post. However, the user may abandon the form entirely or upload a new file instead, so you'd need a strategy for cleaning up these temporary files.
You can follow many diffirent approach for that but i will refer two of them. First, you can carry image to client-side, then again, carry it to backend. You should send the image to backend as well.
[HttpGet]
public IActionResult EditPost(int? id)
{
if (id == null)
{
return View(new PostViewModel());
}
else
{
var post = _repo.GetPost((int)id);
return View(new PostViewModel
{
Id = post.Id,
Text = post.Text,
Image = post.Image
});
}
}
And second, you can only update image when imageUrl is not empty. But this will come with a new condition, "what if client wants to delete the image?". You should decide when you are sure what exactly want to do. I'm adding some uncompleted code just to give some idea.
[HttpPost]
public async Task<IActionResult> EditPost(PostViewModel vm)
{
var post = new Post
{
Id = vm.Id,
Text = vm.Text,
Image = await _fileManager.SaveImage(vm.Image)
};
if (post.Id == 0)
_repo.AddPost(post);
else
{
if (vm.Image == null)
{
//Overwriting viewmodel's image property if empty with old record's image.
var oldRecord = _repo.GetPost(vm.Id);
post.Image = oldRecord?.Image;
}
_repo.UpdatePost(post);
}
if (await _repo.SaveChangesAsync())
return RedirectToAction("Posts");
else
return View(post);
}
Related
I am working on my first project as a junior developer, but i also got the task of creating the backend API. This is therefore my first time touching any backend so i almost dont know any c# or .NET.
Frontend: Nextjs, typescript
Backend: .NET 6, c#
The problem:
The users need to be able to upload an image to an artist object. As of now i am only using a string that represents the path where the image is stored in the frontend.
I will figure out how i will recieve the image in the frontend, and i will create an endpoint in the API where i can PUT the image.
The question im trying to ask is how do i do the recieving and storing of the image in the .NET?
Artist entity:
namespace BookingAPI.Entities
{
public class Artist
{
public string Id { get; set; }
public string Name { get; set; } = null!;
public string Description { get; set; }
public string? Image { get; set; }
}
}
CreateArtist in Controller
// POST /events/{eventId}/program/artists
// Create new artist in Program -> Artists
[HttpPost]
[Route("/events/{eventId}/program/artists")]
public async Task<ActionResult<EventArtistDto>> CreateEventArtistAsync(string eventId, CreateEventArtistDto createEventArtistDto)
{
Artist artist = new()
{
Id = Guid.NewGuid().ToString(),
Name = createEventArtistDto.Name,
Description = createEventArtistDto.Description,
Image = createEventArtistDto.Image,
};
await repository.CreateEventArtistAsync(eventId, artist);
return artist.AsDto(); // add return body?
}
CreateArtost in inMemRepository
public async Task CreateEventArtistAsync(string eventId, Artist artist)
{
var eventIndex = events.FindIndex(existingEvent => existingEvent.Id == eventId);
var existingEvent = events[eventIndex];
var artists = existingEvent.EventProgram.Artists;
artists.Add(artist);
await Task.CompletedTask;
}
Please let me know if i should upload more of the code.
Im really not sure even how to start this code, i am of course googling this at the same time, but though i would make my own question as well so that maybe i could learn this the proper way.
UPDATE:
After reading #EminNiftiyev answer i tried this,
but i get errors:
Controller: "Implicitly-typed variables must be initialized"
InMemRepository: "Cannot implicitly convert type 'Microsoft.AspNetCore.Http.IFormFile' to 'string'"
I dont fully understand what i am doing here.
UpdateImageInEventArtistAsync in Controller
//PUT /events/{eventId}/program/artists/{artistId}/image
// Update the Image in the Artist
[HttpPost]
[Route("/events/{eventId}/program/artists/{artistId}/image")]
public async Task<ActionResult<UpdateImageInEventArtistDto>>
UpdateImageInEventArtistAsync(string eventId, string artistId,
UpdateImageInEventArtistDto updateImageInEventArtistDto,
[FromForm] IFormFile file)
{
// Get Program from Event
var program = await repository.GetEventProgramAsync(eventId);
var existingArtist = program.Artists.Where(artist => artist.Id == artistId).SingleOrDefault();
if (existingArtist is null)
{
return NotFound();
}
var byteArrayImage;
using (var stream = new MemoryStream())
{
await file.CopyToAsync(stream);
byteArrayImage = stream.ToArray();
}
existingArtist.Image = Convert.ToBase64String(byteArrayImage);
/* await repository.UpdateEventArtistAsync(eventId, artistId, existingArtist); */
await repository.UpdateImageInEventArtistAsync(eventId, artistId, byteArrayImage);
return NoContent();
}
UpdateImageInEventArtistAsync in InMemRepository
public async Task UpdateImageInEventArtistAsync(string eventId, string artistId, IFormFile file)
{
var eventIndex = events.FindIndex(existingEvent => existingEvent.Id == eventId);
var existingEvent = events[eventIndex];
var artists = existingEvent.EventProgram.Artists;
var existingArtist = artists.Where(artist => artist.Id == artistId).SingleOrDefault();
existingArtist.Image = file;
await Task.CompletedTask;
}
First you create endPoint for Upload Image. That time perfectly choice is use IFormFile. Code like this
[HttpPost]
[Route("api/image/upload")]
public async Task<IActionResult> UploadImage([FromForm] IFormFile file)
{
}
Then you get image like byteArray format. For this you should use Stream
var byteArrayImage;
using (var stream = new MemoryStream())
{
await file.CopyToAsync(stream);
byteArrayimage = stream.ToArray();
}
Here we go. Now yo can get Image like ByteArray format. And Finally you can insert to Database or another source.
That's perfect practice from another way.
My GET Request with ID as a parameter does not properly load all my data. My PUT Request successfully alters the data according to Swagger but then the GET Requests show that no change has been made. My GET Request with a parameter says my Player data is null when it shouldn't be. My normal GET Request loads the data properly, except when data within the PUT Request changes something within Player[]. See my code below.
Player Model:
public class Player
{
public long Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Teams Model:
public class Team
{
public long Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public IEnumerable<Player> Players { get; set; }
}
Generic GET Request (returns POST'ed data but not updated Player data from PUT Request):
[HttpGet]
public async Task<ActionResult<IEnumerable<Team>>> GetTeams()
{
return await _context.Teams.Include(t => t.Players).ToListAsync();
}
GET Request with ID Parameter (doesn't load Player data):
[HttpGet("{id}")]
public async Task<ActionResult<Team>> GetTeam(long id)
{
var team = await _context.Teams.FindAsync(id);
if (team == null)
{
return NotFound();
}
return team;
}
PUT Request (Alters data):
[HttpPut("{id}")]
public async Task<IActionResult> PutTeam(long id, Team team)
{
if (id != team.Id)
{
return BadRequest();
}
_context.Entry(team).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TeamExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
GET Request with ID Parameter response:
{
"id": 1,
"name": "panthers",
"location": "string",
"players": null
}
Essentially, my PUT Requests returns 204 and Swagger says I've altered the data. The changes specifically within the Player list are not altered, whereas anything aside from it like Location will change just fine. Additionally, the GET Request with ID doesn't return Player at all. GET shows Player data, but not changes within my PUT Request.
To GET Players for a specific team by id, the principle is the same, eager load the navigation property:
var team = await _context.Teams.Include(t => t.Players).FindAsync(id);
For the PUT request, I'd say you should have an Update or UpdateAsync method:
_context.Teams.Update(team);
await _context.SaveChangesAsync();
If this, doesn't work or is unavailable you can try and mark the players as modified as well:
_context.Entry(team).State = EntityState.Modified;
foreach(var player in team.Players)
{
_context.Entry(player).State = EntityState.Modified;
}
await _context.SaveChangesAsync();
If it's still doesn't work you can load the entity, that will track the entity, and detect changes:
var teamFromBd = await _context.Teams.Include(t => t.Players).FindAsync(id);
teamFromBd = team; //*
await _context.SaveChangesAsync();
Make sure the Players are indeed binded to the team method parameter.
* The above is a simplification, you should need to assign the team properties one by one (except fot Ids). You can try as is but it probably won't work.
I am writing a web app using .NET for the backend and React as client. I want to implement the authorization to a login form and thus made two model (DTOs) to send back to the client. One for the login and register respectively. I have an AccountController class where I am handling the Post and Get requests for the current user (user after logged in).
As mentioned above I have a model class named User and its UserDTO. Inside of that model class, I made some props (eg. username, password, profilePictureURL, email) etc. I want the user to upload an image which in turn will get appended to the profilePictureURL into the request back to the server.
This is my User model:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
namespace API.Entities
{
public class User : IdentityUser
{
public IFormFile profilePhotoURL {get; set; }
public string Name { get; set; }
}
}
userName, email and phone numbers are being derived from IdentityUser class from .NET.
registerDTO
using Microsoft.AspNetCore.Http;
namespace API.DTOs
{
public class RegisterDTO : LoginDTO
{
public string Email { get; set; }
public string Name { get; set; }
public FormFile profilePhotoURL { get; set; }
public string PhoneNumber { get; set; }
}
}
Inside of AccountController, my POST method for registering is:
[HttpPost("register"), DisableRequestSizeLimit]
public async Task<IActionResult> RegisterUser(RegisterDTO registerDTO)
{
//getting the file from request
var postedProfile = Request.Form.Files[0];
// setting the Uploads folder
var Uploads = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
if (postedProfile.Length > 0)
{
var fileName = ContentDispositionHeaderValue.Parse(postedProfile.ContentDisposition).FileName.Trim();
var pathToSave = Path.Combine(Uploads, fileName.ToString());
using (var fileStream = new FileStream(pathToSave, FileMode.Create))
{
await postedProfile.CopyToAsync(fileStream);
}
Ok($"File Uploaded successfully");
}
else
{
return BadRequest(new ProblemDetails
{
Title = "400 - Bad Request",
Status = 400,
Detail = "File not uploaded"
});
}
var registeredUser = new User
{
UserName = registerDTO.userName,
Email = registerDTO.Email,
Name = registerDTO.Name,
profilePhotoURL = postedProfile,
PhoneNumber = registerDTO.PhoneNumber
};
var result = await _userManager.CreateAsync(registeredUser, registerDTO.Password);
if (!result.Succeeded)
{
foreach (var Error in result.Errors)
{
ModelState.AddModelError(Error.Code, Error.Description);
}
return ValidationProblem();
}
await _userManager.AddToRoleAsync(registeredUser, "Member");
return StatusCode(201);
}
I want to set the uploaded image into the profilePhotoURL property of my registerDTO class, but when on checking this in swagger, I get the following error:
The JSON value could not be converted to Microsoft.AspNetCore.Http.FormFile. Path: $.profilePhotoURL | LineNumber: 5 | BytePositionInLine: 36.
The above error is in the ModelState errors.
How could I make a method (inside of AccountController's POST request for registering a new user) to upload a file of type IFormFile and then set it inside of the User object?
All suggestions are welcome :)
Your backend was a webapi project, model binding get data from the request body(Json value) by default,but formfile get values from posted form fields.
If you could post a form in your react app,just create a model for the form and add the [FromForm]Attribute
[HttpPost("register"), DisableRequestSizeLimit]
public IActionResult RegisterUser([FromForm] RegisterModel registermodel)
{
return StatusCode(200);
}
I am new to Azure Event Grid and Webhooks.
How can I bind my .net mvc web api application to Microsoft Azure Event Grid?
In short I want, whenever a new file is added to blob storage, Azure Event grid should notify my web api application.
I tried following article but no luck
https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-event-quickstart
How can I bind my .net mvc web api application to Microsoft Azure Event Grid?
In short I want, whenever a new file is added to blob storage, Azure Event grid should notify my web api application.
I do a demo for that, it works correctly on my side. You could refer to the following steps:
1.Create a demo RestAPI project just with function
public string Post([FromBody] object value) //Post
{
return $"value:{value}";
}
2.If we want to intergrate azure storage with Azure Event Grid, we need to create a blob storage account in location West US2 or West Central US. More details could refer to the screen shot.
2.Create Storage Accounts type Event Subscriptions and bind the custom API endpoint
3.Upload the blob to the blob storage and check from the Rest API.
You can accomplish this by creating an custom endpoint that will subscribe to the events published from Event Grid. The documentation you referenced uses Request Bin as a subscriber. Instead create a Web API endpoint in your MVC application to receive the notification. You'll have to support the validation request just to make you have a valid subscriber and then you are off and running.
Example:
public async Task<HttpResponseMessage> Post()
{
if (HttpContext.Request.Headers["aeg-event-type"].FirstOrDefault() == "SubscriptionValidation")
{
using (var reader = new StreamReader(Request.Body, Encoding.UTF8))
{
var result = await reader.ReadToEndAsync();
var validationRequest = JsonConvert.DeserializeObject<GridEvent[]>(result);
var validationCode = validationRequest[0].Data["validationCode"];
var validationResponse = JsonConvert.SerializeObject(new {validationResponse = validationCode});
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(validationResponse)
};
}
}
// Handle normal blob event here
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK };
}
Below is an up-to-date sample of how you would handle it with a Web API. You can also review and deploy a working sample from here: https://github.com/dbarkol/azure-event-grid-viewer
[HttpPost]
public async Task<IActionResult> Post()
{
using (var reader = new StreamReader(Request.Body, Encoding.UTF8))
{
var jsonContent = await reader.ReadToEndAsync();
// Check the event type.
// Return the validation code if it's
// a subscription validation request.
if (EventTypeSubcriptionValidation)
{
var gridEvent =
JsonConvert.DeserializeObject<List<GridEvent<Dictionary<string, string>>>>(jsonContent)
.First();
// Retrieve the validation code and echo back.
var validationCode = gridEvent.Data["validationCode"];
return new JsonResult(new{
validationResponse = validationCode
});
}
else if (EventTypeNotification)
{
// Do more here...
return Ok();
}
else
{
return BadRequest();
}
}
}
public class GridEvent<T> where T: class
{
public string Id { get; set;}
public string EventType { get; set;}
public string Subject {get; set;}
public DateTime EventTime { get; set; }
public T Data { get; set; }
public string Topic { get; set; }
}
You can also use the Microsoft.Azure.EventGrid nuget package.
From the following article (credit to gldraphael): https://gldraphael.com/blog/creating-an-azure-eventgrid-webhook-in-asp-net-core/
[Route("/api/webhooks"), AllowAnonymous]
public class WebhooksController : Controller
{
// POST: /api/webhooks/handle_ams_jobchanged
[HttpPost("handle_ams_jobchanged")] // <-- Must be an HTTP POST action
public IActionResult ProcessAMSEvent(
[FromBody]EventGridEvent[] ev, // 1. Bind the request
[FromServices]ILogger<WebhooksController> logger)
{
var amsEvent = ev.FirstOrDefault(); // TODO: handle all of them!
if(amsEvent == null) return BadRequest();
// 2. Check the eventType field
if (amsEvent.EventType == EventTypes.MediaJobStateChangeEvent)
{
// 3. Cast the data to the expected type
var data = (amsEvent.Data as JObject).ToObject<MediaJobStateChangeEventData>();
// TODO: do your thing; eg:
logger.LogInformation(JsonConvert.SerializeObject(data, Formatting.Indented));
}
// 4. Respond with a SubscriptionValidationResponse to complete the
// event subscription handshake.
if(amsEvent.EventType == EventTypes.EventGridSubscriptionValidationEvent)
{
var data = (amsEvent.Data as JObject).ToObject<SubscriptionValidationEventData>();
var response = new SubscriptionValidationResponse(data.ValidationCode);
return Ok(response);
}
return BadRequest();
}
}
I was wondering whether it is possible to send a file (which I only want ".pdf", ".jpg" or ".png") along with raw JSON.
All my endpoints so far send raw JSON (which I'm testing via Postman as the frontend does not exist yet), the intention is that the sending of form data will be sent using Angular Js. I don't know Angular Js yet, so I can't imagine how this will work.
The signature of the endpoint in question looks like this:
[Route("Post")]
[CustomAuthorize(Roles = "User, Admin")]
[ValidateJWT]
public async Task<IHttpActionResult> Post(HttpRequestMessage request, SalesOrderViewModel orderData)
The view model is just a c# class with loads of string properties that the model binder converts from JSON.
I would like to know whether sending raw JSON and a file that the user will select is possible in the same endpoint with Web API 2.
Is it?
Thanks in advance.
You can't direct with a post as aplication/json, still you can do it with multiple form fields (as form data), file + data, where the value of data can be a JSON.
I'm not recommending this method but does the trick:
public async Task<IHttpActionResult> Post()
{
if (!Request.Content.IsMimeMultipartContent())
{
Request.CreateResponse(HttpStatusCode.UnsupportedMediaType);
}
//load in a memory stream or in azure blob storage
var uploadFolder = "~/App_Data/FileUploads"; // to demonstrate the upload so please don't comment about where I'm saving the file, don't recommend this under no circumstance
var root = HttpContext.Current.Server.MapPath(uploadFolder);
Directory.CreateDirectory(root);
var provider = new MultipartFormDataStreamProvider(root);
var result = await Request.Content.ReadAsMultipartAsync(provider);
if (result.FileData.FirstOrDefault() == null)
{
return BadRequest("No import file was attached");
}
var uploadedFileInfo = new FileInfo(result.FileData.First().LocalFileName);
var model = result.FormData["model"];
if (model == null)
{
return BadRequest("Model is missing");
}
var parameters = JsonConvert.DeserializeObject<Coords>(model);
var byteArray = File.ReadAllBytes(uploadedFileInfo.FullName);
//..process the bytes
//..process json passed in headers
}
And the model:
public class Coords
{
public Cord[] cords { get; set; }
}
public class Cord
{
public int x { get; set; }
public object y { get; set; }
}
Postman call:
Edit: Just checked you don't have to use IEnumerable<byte>. Using byte[] works fine.
WebApi supports deserializing JSON array to IEnumerable<T>, so that you can receive bytes by declaring with IEnumerable<byte>.
The following example will show how to upload image:
public class ImageModel
{
public string Name { get; set; }
public IEnumerable<byte> Bytes { get; set; }
}
In your controller. Writing image to disk:
private string WriteImage(byte[] arr)
{
var filename = $#"images\{DateTime.Now.Ticks}.";
using (var im = Image.FromStream(new MemoryStream(arr)))
{
ImageFormat frmt;
if (ImageFormat.Png.Equals(im.RawFormat))
{
filename += "png";
frmt = ImageFormat.Png;
}
else
{
filename += "jpg";
frmt = ImageFormat.Jpeg;
}
string path = HttpContext.Current.Server.MapPath("~/") + filename;
im.Save(path, frmt);
}
return $#"http:\\{Request.RequestUri.Host}\{filename}";
}
HttpContext.Current.Server.MapPath("~/") will give the internal path of server running. Request.RequestUri.Host returns the hostname.
public IHttpActionResult UploadImage(ImageModel model)
{
var imgUrl = WriteImage(model.Bytes.ToArray());
// Some code
}
In HTML:
<input type="file" id="imageFile"/>
Upload method for AngularJS:
$scope.upload = function () {
var file = document.getElementById("imageFile").files[0];
var r = new FileReader();
r.onloadend = function (e) {
var arr = Array.from(new Uint8Array(e.target.result));
var uploadData = {
Name: "Name of Image",
Bytes: arr
}
console.log(uploadData);
$http.post('api/Uploader/UploadImage', uploadData)
.then(
function (response) {
console.log(response);
},
function (reason) {
console.log(reason);
})
}
r.readAsArrayBuffer(file);
}