1

In my MVC application, I have two entities called Ticket and Attachment, and when inserting a new ticket record, I also want to insert new records for attachments with the FK of previously inserted Ticket's ID.

I have a look at some samples as this page, but I need to use a loop for the multiple attachments in a single method so that users can attach multiple files when creating a new ticket. Could you give a sample stored procedure or a sample method for Entity Framework to solve this problem?

Here are these two entities:

Ticket:

public class Ticket
{
    [Key] 
    public int ID { get; set; }

    public string Comment { get; set; }

    //Navigation Property
    public virtual ICollection<Attachment> Attachments { get; set; }

}


Attachment:

public class Attachment
{
    [Key]
    public int ID { get; set; }

    //Foreign key for Ticket
    public int TicketID { get; set; }   

    public byte[] FileData { get; set; }

    public string FileMimeType { get; set; }

    //Navigation Property 
    public virtual Ticket Ticket { get; set; }
}
Community
  • 1
  • 1
Jack
  • 1
  • 21
  • 118
  • 236
  • Sorry, could you clarify what you're trying to do inside the loop? Are you trying to attach x Attachments to a Ticket, more or less? I think this will be done with an "ON INSERT" trigger. – Curmudgeon Jun 11 '15 at 23:37
  • Are you wanting a SQL solution, an Entity Framework solution, or are you asking how to construct a Stored Procedure in SQL that Entity Framework can call upon? This can be done all three ways, but the complexity and performance varies among the approaches. A bit of clarification of what you currently have would be helpful. – Claies Jun 11 '15 at 23:53
  • @Curmudgeon I want to insert each attachment to the Attachment table (I return an attachment list from controller. A user can attach multiple files when creating a new ticket). So, in a single method, I need to call first SaveChanges() for Ticket and then for Atachment with a loop. I think Ladislav Mrnka's answer on [this](http://stackoverflow.com/questions/5212751/how-can-i-get-id-of-inserted-entity-in-entity-framework) page seems to be the most suitable for my needs, but I need to use a tarnsaction and loop in it. Could you modify it and post here as answer? – Jack Jun 11 '15 at 23:54
  • @Claies I would prefer Entity Framework solution with SaveChanges() method. As I said the previous message, it might be better for me to use ObjectSet to obtain the id of newly created record and use Transaction in a single method. On the other hand, if you do not have any idea for such a kind of solution I would be happy if to get your suggestions with SP solution. If it is possible, of course would be best to post all the possible solution so that the other people get benefit from all of them. Thanks in advance. – Jack Jun 12 '15 at 00:00
  • @marc_s Thanks for edit. Any idea regarding to problem? – Jack Jun 12 '15 at 00:06
  • why do you even need to do it that way? I think you are trying to over-engineer a solution, since you can actually do this quite easily in Entity Framework without needing the `Id` yourself. – Claies Jun 12 '15 at 00:07
  • 1
    Create the `Ticket` object, create any `Attachment` objects and add them to the navigation property (most likely called `Attachments`), and then save the `Ticket` object using EF - EF should handle all the foreign key magic for you - can't get any simpler than that ... – marc_s Jun 12 '15 at 00:08
  • @marc_s Actually I have already applied the relationships and defined the navigation properties on both models. But I have never though such a kind of solution. I updated my code above. Could you have a look at and inform me please if there is a problem? On the other hand I have really no idea about the method that has SaveChanges() call and Transactions. Could you give an example please? Thanks in advance. – Jack Jun 12 '15 at 00:20
  • @Claies I have no idea regarding to the method containing SaveChanges() call and Transactions. Could you give an example please? Thanks in advance. – Jack Jun 12 '15 at 00:23
  • again, I think you are seriously overthinking this. You should only need to call `SaveChanges()` one time, and this doesn't appear to be any kind of data that needs a Transaction. Just create your `Ticket`, add your `Attachment` data to the `Ticket`, `SaveChanges()`. The assignment of ID's for both the PK and the FK for the objects should be handled completely automatically. – Claies Jun 12 '15 at 00:28
  • @Claies Ok, you are right. But I have no idea regarding to the method that should be used for Controller and data layer. Could you give an example please? On the other hand, what if there is a problem during inserting any of the attachments after inserting ticket? As I insert them in a method call, should I use transaction? – Jack Jun 12 '15 at 00:34
  • `SaveChanges()` will roll back that transaction and throw an exception if any of the dirty `ObjectStateEntry` objects cannot be persisted. Since you are performing all your database changes in a single `SaveChanges()` call which would execute a single SQL query, if the query fails, the database will be in the same state. The transaction is implied. – Claies Jun 12 '15 at 00:39
  • @marc_s I have applied the answer below but did not succeeded. If I call SaveChanges for Ticket, is it possible to rollback this when I call SaveChanges again for the Attachment in the same method? Could you please post a sample code as answer by showing such a kind of transaction? Thanks in advance. – Jack Jun 12 '15 at 09:04

3 Answers3

1

Take a look at AssociationAttribute in Entity Framework. It will allow you to set an object to a property in order to set the foreign key. If you add something like:

public class Attachment {
    ...
    [Association(Name = "ticket", ThisKey = "TicketId", OtherKey = "Id", IsForeignKey = true)]
    public Ticket Ticket { get; set;}
    [Column(DBType = "INT NOT NULL" ...]
    public int TicketId { get; set; }
    ...
}

to your class, then you should be able to do assign a ticket object to you attachment object's "Ticket" property like this:

public UploadTicket(string ticketComment, List<Attachment> attachments) {
    (using db = new DataContext()) {
        var ticket = new ticket();
        ticket.Comment = ticketComment;
        db.Tickets.InsertOnSubmit(ticket);
        foreach (var att in attachments) {
            var attachment = new attachment();
            attachment.Ticket = ticket; // <- this will assign the TicketId property automatically upon insert
            attachment.FileData = att.FileData;
            attachment.FileMimeType = att.FileMimeType;
            db.Attachments.InsertOnSubmit(attachment);
        }
        db.SubmitChanges();
    }
}
Mark
  • 172
  • 7
  • 1
    you don't even need the `AssociationAttribute` in this scenario, because the FK is automatically implied by naming convention. – Claies Jun 12 '15 at 00:26
  • As @Claies said, I do not need to use it because relation is automatically defined because of defining the FK column [TableName + ID] (TicketID). So, for the UploadTicket method, what about the transaction? Should I use transaction just in case there is a problem for inserting any of the attachments? – Jack Jun 12 '15 at 00:30
  • LINQ to SQL automatically groups all `InsertOnSubmit()` into one transaction until you call `SubmitChanges()`. If any of the attachments fail upon inserting, the whole thing gets rolled back. – Mark Jun 12 '15 at 00:43
  • also note that this is the EF 4 implementation (DataContext) but the same technique works with EF 5+ (ObjectContext). – Claies Jun 12 '15 at 00:48
  • @Claies There is a problem when using InsertOnSubmit and I am not sure if I have to use it. On the other hand, if I call SaveChanges for Ticket, is it possible to rollback this when I call SaveChanges again for the Attachment? Why do not you want to post a sample code as answer so that the other people also use this? Thanks in advance. – Jack Jun 12 '15 at 10:34
1

Try something like this (this is simplified, of course - but it shows the basic mechanism that should be used - instantiating objects and connecting them via their navigation properties, and saving only once for the whole object graph):

// establish the DbContext
using (TicketModel ctx = new TicketModel())
{
    // create a ticket
    Ticket t1 = new Ticket
    {
        Comment = "This is ticket #1"
    };

    // add two attachments to it
    Attachment a1 = new Attachment { FileMimeType = "text/json" };
    t1.Attachment.Add(a1);

    Attachment a2 = new Attachment { FileMimeType = "application/octet-stream" };
    t1.Attachment.Add(a2);

    // add the ticket to the context
    ctx.Ticket.Add(t1);

    // save everything
    ctx.SaveChanges();
}

With this, you should have a ticket in your database, and two connected attachments (with their TicketID column set to the correct value for the ticket)

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
  • 1
    Many thanks for your help. I posted the final code so that some other people can have a look at this approach. – Jack Jun 14 '15 at 12:54
1

By improving the mechanism that marc_s suggested I solved the problem. For those who need such a kind of solution I posted the final code:

Method in Controller:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult _Create([Bind(Exclude = null)] IssueViewModel ticketViewModel, IEnumerable<HttpPostedFileBase> files)
{
    try
    {
        if (ModelState.IsValid)
        {
            ticketViewModel.FileAttachments = new List<FileAttachment>();
            foreach (var upload in files)
            {
                if (upload != null && upload.ContentLength > 0)
                {
                    if (upload.ContentType == "application/pdf" 
|| upload.ContentType == "application/vnd.openxmlformats-officedocument.word" 
|| upload.ContentType == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                    {
                        FileAttachment fileAttachment = new FileAttachment
                        {
                            Created = DateTime.Now,
                            FileMimeType = upload.ContentType,
                            FileData = new byte[upload.ContentLength]
                        };
                        upload.InputStream.Read(fileAttachment.FileData, 0, upload.ContentLength);
                         ticketViewModel.FileAttachments.Add(fileAttachment);
                    }
                    else
                    {
                        return PartialView("_Create");
                    }
                }
            }
            repository.SaveTicket(ticketViewModel.Issue, ticketViewModel.FileAttachments);
            return RedirectToAction("Completed");
        }
    }
    catch (RetryLimitExceededException /* dex */)
    {
        //Log the error (uncomment dex variable name and add a line here to write a log.)
        ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
    }      
    return View(ticketViewModel);
}


Method in Data Layer:

public void SaveTicket(Ticket ticket, IEnumerable<FileAttachment> fileAttachment)
{
    context.Tickets.Add(ticket);
    foreach (FileAttachment f in fileAttachment)
    {
        context.FileAttachments.Add(f);
    }
    context.SaveChanges();
}
Jack
  • 1
  • 21
  • 118
  • 236